如何迭代遍历std::tuple的元素?

161

我该如何使用C++11迭代元组?我尝试了以下代码:

for(int i=0; i<std::tuple_size<T...>::value; ++i) 
  std::get<i>(my_tuple).do_sth();

但是这样不起作用:

错误 1:抱歉,未实现:无法将“Listener …”扩展为固定长度的参数列表。
错误 2:i 不能出现在常量表达式中。

那么,我应该如何正确地迭代元组中的元素?


2
我可以问一下,你是如何编译C++0x的?据我所知,它还没有发布或准备好。 - Burkhard
6
g++自版本4.3开始包含了对某些C++0X特性的实验性支持,包括可变参数模板。其他编译器也有类似的做法(但功能集可能不同),如果你要在生产环境中使用这些新特性,那么你将会回到90年代,因为对于前沿技术的支持存在很大的差异。 - AProgrammer
我正在使用带有std=c++0x的g++ 4.4版本。 - 1521237
9
这个问题需要进行 C++11 更新。 - Omnifarious
2
@Omnifarious 现在需要进行C++14更新 - oblitum
可以在 stack exchange 上找到一个 C++14 的解决方案。 - yves
24个回答

5

如果您想使用std :: tuple,并且您有支持可变模板的C ++编译器,请尝试下面的代码(已在g++4.5上测试)。这应该是您问题的答案。

#include <tuple>

// ------------- UTILITY---------------
template<int...> struct index_tuple{}; 

template<int I, typename IndexTuple, typename... Types> 
struct make_indexes_impl; 

template<int I, int... Indexes, typename T, typename ... Types> 
struct make_indexes_impl<I, index_tuple<Indexes...>, T, Types...> 
{ 
    typedef typename make_indexes_impl<I + 1, index_tuple<Indexes..., I>, Types...>::type type; 
}; 

template<int I, int... Indexes> 
struct make_indexes_impl<I, index_tuple<Indexes...> > 
{ 
    typedef index_tuple<Indexes...> type; 
}; 

template<typename ... Types> 
struct make_indexes : make_indexes_impl<0, index_tuple<>, Types...> 
{}; 

// ----------- FOR EACH -----------------
template<typename Func, typename Last>
void for_each_impl(Func&& f, Last&& last)
{
    f(last);
}

template<typename Func, typename First, typename ... Rest>
void for_each_impl(Func&& f, First&& first, Rest&&...rest) 
{
    f(first);
    for_each_impl( std::forward<Func>(f), rest...);
}

template<typename Func, int ... Indexes, typename ... Args>
void for_each_helper( Func&& f, index_tuple<Indexes...>, std::tuple<Args...>&& tup)
{
    for_each_impl( std::forward<Func>(f), std::forward<Args>(std::get<Indexes>(tup))...);
}

template<typename Func, typename ... Args>
void for_each( std::tuple<Args...>& tup, Func&& f)
{
   for_each_helper(std::forward<Func>(f), 
                   typename make_indexes<Args...>::type(), 
                   std::forward<std::tuple<Args...>>(tup) );
}

template<typename Func, typename ... Args>
void for_each( std::tuple<Args...>&& tup, Func&& f)
{
   for_each_helper(std::forward<Func>(f), 
                   typename make_indexes<Args...>::type(), 
                   std::forward<std::tuple<Args...>>(tup) );
}

boost::fusion是另一个选择,但需要使用自己的元组类型:boost::fusion::tuple。我们最好坚持使用标准!这里是一份测试:

#include <iostream>

// ---------- FUNCTOR ----------
struct Functor 
{
    template<typename T>
    void operator()(T& t) const { std::cout << t << std::endl; }
};

int main()
{
    for_each( std::make_tuple(2, 0.6, 'c'), Functor() );
    return 0;
}

可变参数模板的威力!


我尝试了你的第一个解决方案,但它在处理这个函数对时失败了。有什么想法吗?模板 <typename T, typename U> void addt(pair<T,U> p) { cout << p.first + p.second << endl; }int main(int argc, char *argv[]) { cout << "Hello." << endl; for_each(make_tuple(2,3,4), [](int i) { cout << i << endl; }); for_each(make_tuple(make_pair(1,2),make_pair(3,4)), addt); return 0; } - user2023370
这个答案写得太啰嗦了,真是遗憾,因为我认为迭代的方式(for_each_impl)是我见过的所有解决方案中最优雅的。 - joki

5
另一个选项是实现元组迭代器。这样做的好处是可以使用标准库提供的各种算法和基于范围的for循环。一种优雅的方法在这里 https://foonathan.net/2017/03/tuple-iterator/ 中解释。其基本思想是将元组转换为一个范围,具有 begin()end() 方法来提供迭代器。迭代器本身返回一个std::variant<...>,然后可以使用std::visit进行访问。

以下是一些示例:

auto t = std::tuple{ 1, 2.f, 3.0 };
auto r = to_range(t);

for(auto v : r)
{
    std::visit(unwrap([](auto& x)
        {
            x = 1;
        }), v);
}

std::for_each(begin(r), end(r), [](auto v)
    {
        std::visit(unwrap([](auto& x)
            {
                x = 0;
            }), v);
    });

std::accumulate(begin(r), end(r), 0.0, [](auto acc, auto v)
    {
        return acc + std::visit(unwrap([](auto& x)
        {
            return static_cast<double>(x);
        }), v);
    });

std::for_each(begin(r), end(r), [](auto v)
{
    std::visit(unwrap([](const auto& x)
        {
            std::cout << x << std::endl;
        }), v);
});

std::for_each(begin(r), end(r), [](auto v)
{
    std::visit(overload(
        [](int x) { std::cout << "int" << std::endl; },
        [](float x) { std::cout << "float" << std::endl; },
        [](double x) { std::cout << "double" << std::endl; }), v);
});

我的实现(严重基于上面链接中的解释):

#ifndef TUPLE_RANGE_H
#define TUPLE_RANGE_H

#include <utility>
#include <functional>
#include <variant>
#include <type_traits>

template<typename Accessor>
class tuple_iterator
{
public:
    tuple_iterator(Accessor acc, const int idx)
        : acc_(acc), index_(idx)
    {

    }

    tuple_iterator operator++()
    {
        ++index_;
        return *this;
    }

    template<typename T>
    bool operator ==(tuple_iterator<T> other)
    {
        return index_ == other.index();
    }

    template<typename T>
    bool operator !=(tuple_iterator<T> other)
    {
        return index_ != other.index();
    }

    auto operator*() { return std::invoke(acc_, index_); }

    [[nodiscard]] int index() const { return index_; }

private:
    const Accessor acc_;
    int index_;
};

template<bool IsConst, typename...Ts>
struct tuple_access
{
    using tuple_type = std::tuple<Ts...>;
    using tuple_ref = std::conditional_t<IsConst, const tuple_type&, tuple_type&>;

    template<typename T>
    using element_ref = std::conditional_t<IsConst,
        std::reference_wrapper<const T>,
        std::reference_wrapper<T>>;

    using variant_type = std::variant<element_ref<Ts>...>;
    using function_type = variant_type(*)(tuple_ref);
    using table_type = std::array<function_type, sizeof...(Ts)>;

private:
    template<size_t Index>
    static constexpr function_type create_accessor()
    {
        return { [](tuple_ref t) -> variant_type
        {
            if constexpr (IsConst)
                return std::cref(std::get<Index>(t));
            else
                return std::ref(std::get<Index>(t));
        } };
    }

    template<size_t...Is>
    static constexpr table_type create_table(std::index_sequence<Is...>)
    {
        return { create_accessor<Is>()... };
    }

public:
    static constexpr auto table = create_table(std::make_index_sequence<sizeof...(Ts)>{}); 
};

template<bool IsConst, typename...Ts>
class tuple_range
{
public:
    using tuple_access_type = tuple_access<IsConst, Ts...>;
    using tuple_ref = typename tuple_access_type::tuple_ref;

    static constexpr auto tuple_size = sizeof...(Ts);

    explicit tuple_range(tuple_ref tuple)
        : tuple_(tuple)
    {
    }

    [[nodiscard]] auto begin() const 
    { 
        return tuple_iterator{ create_accessor(), 0 };
    }

    [[nodiscard]] auto end() const 
    { 
        return tuple_iterator{ create_accessor(), tuple_size };
    }

private:
    tuple_ref tuple_;

    auto create_accessor() const
    { 
        return [this](int idx)
        {
            return std::invoke(tuple_access_type::table[idx], tuple_);
        };
    }
};

template<bool IsConst, typename...Ts>
auto begin(const tuple_range<IsConst, Ts...>& r)
{
    return r.begin();
}

template<bool IsConst, typename...Ts>
auto end(const tuple_range<IsConst, Ts...>& r)
{
    return r.end();
}

template <class ... Fs>
struct overload : Fs... {
    explicit overload(Fs&&... fs) : Fs{ fs }... {}
    using Fs::operator()...;

    template<class T>
    auto operator()(std::reference_wrapper<T> ref)
    {
        return (*this)(ref.get());
    }

    template<class T>
    auto operator()(std::reference_wrapper<const T> ref)
    {
        return (*this)(ref.get());
    }
};

template <class F>
struct unwrap : overload<F>
{
    explicit unwrap(F&& f) : overload<F>{ std::forward<F>(f) } {}
    using overload<F>::operator();
};

template<typename...Ts>
auto to_range(std::tuple<Ts...>& t)
{
    return tuple_range<false, Ts...>{t};
}

template<typename...Ts>
auto to_range(const std::tuple<Ts...>& t)
{
    return tuple_range<true, Ts...>{t};
}


#endif

通过将const std::tuple<>&传递给to_range(),还支持只读访问。


4
在MSVC STL中有一个未记录的_For_each_tuple_element函数(针对IT技术相关内容)。
#include <tuple>

// ...

std::tuple<int, char, float> values{};
std::_For_each_tuple_element(values, [](auto&& value)
{
    // process 'value'
});

3
使用 constexprif constexpr(C++17),这相当简单和直接:
template <std::size_t I = 0, typename ... Ts>
void print(std::tuple<Ts...> tup) {
  if constexpr (I == sizeof...(Ts)) {
    return;
  } else {
    std::cout << std::get<I>(tup) << ' ';
    print<I+1>(tup);
  }
}

如果我们有 if constexpr我们也有 std::apply(),它比使用递归模板更容易。 - Toby Speight

2

其他人已经提到了一些设计良好的第三方库,你可以使用它们。但是,如果你在不使用这些第三方库的情况下使用C ++,以下代码可能会有所帮助。

namespace detail {

template <class Tuple, std::size_t I, class = void>
struct for_each_in_tuple_helper {
  template <class UnaryFunction>
  static void apply(Tuple&& tp, UnaryFunction& f) {
    f(std::get<I>(std::forward<Tuple>(tp)));
    for_each_in_tuple_helper<Tuple, I + 1u>::apply(std::forward<Tuple>(tp), f);
  }
};

template <class Tuple, std::size_t I>
struct for_each_in_tuple_helper<Tuple, I, typename std::enable_if<
    I == std::tuple_size<typename std::decay<Tuple>::type>::value>::type> {
  template <class UnaryFunction>
  static void apply(Tuple&&, UnaryFunction&) {}
};

}  // namespace detail

template <class Tuple, class UnaryFunction>
UnaryFunction for_each_in_tuple(Tuple&& tp, UnaryFunction f) {
  detail::for_each_in_tuple_helper<Tuple, 0u>
      ::apply(std::forward<Tuple>(tp), f);
  return std::move(f);
}

注意:该代码可与支持C++11的任何编译器编译,并保持与标准库设计的一致性:
  1. 元组不必是std::tuple,而可以是任何支持std::getstd::tuple_size的类型;特别地,可以使用std::arraystd::pair

  2. 元组可以是引用类型或cv限定符;

  3. 它的行为类似于std::for_each,并返回输入的UnaryFunction

  4. 对于C++14(或更高版本)用户,typename std::enable_if<T>::typetypename std::decay<T>::type可以替换为简化的版本std::enable_if_t<T>std::decay_t<T>

  5. 对于C++17(或更高版本)用户,std::tuple_size<T>::value可以替换为其简化版本std::tuple_size_v<T>

  6. 对于C++20(或更高版本)用户,SFINAE功能可以使用Concepts实现。


1

在我看过的所有答案中,这里这里,我最喜欢@sigidagi迭代的方式。不幸的是,他的答案非常冗长,我认为这掩盖了内在的清晰度。

这是我对他解决方案的版本,更加简洁,并且适用于std::tuplestd::pairstd::array

template<typename UnaryFunction>
void invoke_with_arg(UnaryFunction)
{}

/**
 * Invoke the unary function with each of the arguments in turn.
 */
template<typename UnaryFunction, typename Arg0, typename... Args>
void invoke_with_arg(UnaryFunction f, Arg0&& a0, Args&&... as)
{
    f(std::forward<Arg0>(a0));
    invoke_with_arg(std::move(f), std::forward<Args>(as)...);
}

template<typename Tuple, typename UnaryFunction, std::size_t... Indices>
void for_each_helper(Tuple&& t, UnaryFunction f, std::index_sequence<Indices...>)
{
    using std::get;
    invoke_with_arg(std::move(f), get<Indices>(std::forward<Tuple>(t))...);
}

/**
 * Invoke the unary function for each of the elements of the tuple.
 */
template<typename Tuple, typename UnaryFunction>
void for_each(Tuple&& t, UnaryFunction f)
{
    using size = std::tuple_size<typename std::remove_reference<Tuple>::type>;
    for_each_helper(
        std::forward<Tuple>(t),
        std::move(f),
        std::make_index_sequence<size::value>()
    );
}

演示:coliru

C++14的std::make_index_sequence可以实现用于C++11


1
我可能错过了这趟列车,但这将作为未来参考留在这里。 这是基于答案gist的构造:
#include <tuple>
#include <utility>

template<std::size_t N>
struct tuple_functor
{
    template<typename T, typename F>
    static void run(std::size_t i, T&& t, F&& f)
    {
        const std::size_t I = (N - 1);
        switch(i)
        {
        case I:
            std::forward<F>(f)(std::get<I>(std::forward<T>(t)));
            break;

        default:
            tuple_functor<I>::run(i, std::forward<T>(t), std::forward<F>(f));
        }
    }
};

template<>
struct tuple_functor<0>
{
    template<typename T, typename F>
    static void run(std::size_t, T, F){}
};

您可以将其用作以下方式:

template<typename... T>
void logger(std::string format, T... args) //behaves like C#'s String.Format()
{
    auto tp = std::forward_as_tuple(args...);
    auto fc = [](const auto& t){std::cout << t;};

    /* ... */

    std::size_t some_index = ...
    tuple_functor<sizeof...(T)>::run(some_index, tp, fc);

    /* ... */
}

还有改进的空间。


根据 OP 的代码,它将变成这样:


const std::size_t num = sizeof...(T);
auto my_tuple = std::forward_as_tuple(t...);
auto do_sth = [](const auto& elem){/* ... */};
for(int i = 0; i < num; ++i)
    tuple_functor<num>::run(i, my_tuple, do_sth);

1

在 @Stypox 的回答基础上,我们可以使他们的解决方案更加通用(C++17 及以上版本)。通过添加一个可调用的函数参数:

template<size_t I = 0, typename... Tp, typename F>
void for_each_apply(std::tuple<Tp...>& t, F &&f) {
    f(std::get<I>(t));
    if constexpr(I+1 != sizeof...(Tp)) {
        for_each_apply<I+1>(t, std::forward<F>(f));
    }
}

然后,我们需要一种策略来访问每种类型。
让我们从一些辅助工具开始(前两个取自cppreference):
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
template<class ... Ts> struct variant_ref { using type = std::variant<std::reference_wrapper<Ts>...>; };

variant_ref用于允许修改元组的状态。

用法:

std::tuple<Foo, Bar, Foo> tuples;

for_each_apply(tuples,
               [](variant_ref<Foo, Bar>::type &&v) {
                   std::visit(overloaded {
                       [](Foo &arg) { arg.foo(); },
                       [](Bar const &arg) { arg.bar(); },
                   }, v);
               });

结果:

Foo0
Bar
Foo0
Foo1
Bar
Foo1

为了完整起见,这里是我的BarFoo:
struct Foo {
    void foo() {std::cout << "Foo" << i++ << std::endl;}
    int i = 0;
};
struct Bar {
    void bar() const {std::cout << "Bar" << std::endl;}
};

我的问题是为什么std::apply看起来像这样。我认为这个for_each_apply很直接,能够满足你的需求。请求标准化for_each_apply - user877329

0

有很多好的答案,但出于某种原因,大多数答案都没有考虑将应用 f 到我们的元组后返回结果...或者我是忽略了吗?无论如何,这里是另一种你可以做到这一点的方法:

以风格(有争议)进行 Foreach

auto t = std::make_tuple(1, "two", 3.f);
t | foreach([](auto v){ std::cout << v << " "; });

然后从那里返回:

    auto t = std::make_tuple(1, "two", 3.f);
    auto sizes = t | foreach([](auto v) {
        return sizeof(v);
    });
    sizes | foreach([](auto v) {
        std::cout << v;
    });

实现(相当简单)

编辑:变得有点混乱了。

我不会在这里包含一些元编程样板,因为它肯定会使事情变得不太可读,而且我相信那些已经在stackoverflow的某个地方得到了回答。 如果你感到懒惰,可以随意查看我的github repo,以获取两者的实现。

#include <utility>


// Optional includes, if you don't want to implement it by hand or google it
// you can find it in the repo (link below)
#include "typesystem/typelist.hpp"
// used to check if all return types are void, 
// making it a special case 
// (and, alas, not using constexpr-if 
//    for the sake of being compatible with C++14...) 


template <bool Cond, typename T, typename F>
using select = typename std::conditional<Cond, T, F>::type;


template <typename F>
struct elementwise_apply {
    F f;
};

template <typename F>
constexpr auto foreach(F && f) -> elementwise_apply<F> { return {std::forward<F>(f)}; }


template <typename R>
struct tuple_map {
    template <typename F, typename T, size_t... Is>
    static constexpr decltype(auto) impl(std::index_sequence<Is...>, F && f, T&& tuple) {
        return R{ std::forward<F>(f)( std::get<Is>(tuple) )... };
    }
};

template<>
struct tuple_map<void> {
    template <typename F, typename T, size_t... Is>
    static constexpr void impl(std::index_sequence<Is...>, F && f, T&& tuple) {
        [[maybe_unused]] std::initializer_list<int> _ {((void)std::forward<F>(f)( std::get<Is>(tuple) ), 0)... };
    }
};

template <typename F, typename... Ts>
constexpr decltype(auto) operator| (std::tuple<Ts...> & t, fmap<F> && op) {
    constexpr bool all_void = core::Types<decltype( std::move(op).f(std::declval<Ts&>()) )...>.all( core::is_void );
    using R = meta::select<all_void, void, std::tuple<decltype(std::move(op).f(std::declval<Ts&>()))...>>;
    return tuple_map<R>::impl(std::make_index_sequence<sizeof...(Ts)>{}, std::move(op).f, t);
}

template <typename F, typename... Ts>
constexpr decltype(auto) operator| (std::tuple<Ts...> const& t, fmap<F> && op) {
    constexpr bool all_void = check if all "decltype( std::move(op).f(std::declval<Ts>()) )..." types are void, since then it's a special case
    // e.g. core::Types<decltype( std::move(op).f(std::declval<Ts>()) )...>.all( core::is_void );
    using R = meta::select<all_void, void, std::tuple<decltype(std::move(op).f(std::declval<Ts const&>()))...>>;
    return tuple_map<R>::impl(std::make_index_sequence<sizeof...(Ts)>{}, std::move(op).f, t);
}

template <typename F, typename... Ts>
constexpr decltype(auto) operator| (std::tuple<Ts...> && t, fmap<F> && op) {
    constexpr bool all_void = core::Types<decltype( std::move(op).f(std::declval<Ts&&>()) )...>.all( core::is_void );
    using R = meta::select<all_void, void, std::tuple<decltype(std::move(op).f(std::declval<Ts&&>()))...>>;
    return tuple_map<R>::impl(std::make_index_sequence<sizeof...(Ts)>{}, std::move(op).f, std::move(t));
}

是的,如果我们使用C++17会更好。

这也是std::moving对象成员的一个例子,我将更好地参考这篇不错的简短文章

P.S. 如果你卡在检查所有“decltype(std::move(op).f(std::declval()))…”类型是否为void上, 你可以找到一些元编程库,或者,如果那些库似乎太难理解(其中一些可能由于一些疯狂的元编程技巧),你知道该去哪里看。


0

以下是基于 std::interger_sequence 的解决方案。

由于我不知道你的代码中是否使用了std::make_tuple<T>(T &&...)来构造my_tuple。这对下面如何构造std::integer_sequence至关重要。

(1)如果你已经在函数外部有一个my_tuple(不使用template<typename ...T>),那么可以使用以下方法:

[](auto my_tuple)
{
    [&my_tuple]<typename N, N... n>(std::integer_sequence<N, n...> int_seq)
    {
        ((std::cout << std::get<n>(my_tuple) << '\n'), ...);
    }(std::make_index_sequence<std::tuple_size_v<decltype(my_tuple)>>{});
}(std::make_tuple());

(2) 如果您的函数中没有构建my_tuple并且想要处理您的T ...arguments

[]<typename ...T>(T... args)
{
    [&args...]<typename N, N... n>(std::integer_sequence<N, n...> int_seq)
    {
        ((std::cout << std::get<n>(std::forward_as_tuple(args...)) << '\n'), ...);
    }(std::index_sequence_for<T...>{});
}();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接