如何迭代遍历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个回答

149

我基于遍历元组的答案:

#include <tuple>
#include <utility> 
#include <iostream>

template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp), void>::type
  print(std::tuple<Tp...>& t)
  { }

template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I < sizeof...(Tp), void>::type
  print(std::tuple<Tp...>& t)
  {
    std::cout << std::get<I>(t) << std::endl;
    print<I + 1, Tp...>(t);
  }

int
main()
{
  typedef std::tuple<int, float, double> T;
  T t = std::make_tuple(2, 3.14159F, 2345.678);

  print(t);
}

通常的想法是使用编译时递归。实际上,正如原始元组论文中所指出的那样,这个想法被用来制作类型安全的printf。
这可以很容易地推广到元组的 for_each 中:
#include <tuple>
#include <utility> 

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp), void>::type
  for_each(std::tuple<Tp...> &, FuncT) // Unused arguments are given no names.
  { }

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if<I < sizeof...(Tp), void>::type
  for_each(std::tuple<Tp...>& t, FuncT f)
  {
    f(std::get<I>(t));
    for_each<I + 1, FuncT, Tp...>(t, f);
  }

虽然这需要一些努力来让 FuncT 表示出每种类型的元组所需的适当重载。如果您知道所有元组元素将共享一个公共基类或类似的东西,则效果最佳。


8
感谢提供这个简单易懂的例子。对于初学C++且想了解其工作原理的人,请查看SFINAEenable_if文档 - Faheem Mitha
4
我已经添加了这个概括,因为我实际上需要它,并且我认为让其他人看到会很有用。 - Omnifarious
2
请注意:您可能还需要带 const std::tuple<Tp...>& 版本的代码。如果您在迭代时不打算修改元组,则这些 const 版本就足够了。 - lethal-guitar
非常感谢您提供的好解决方案 :-). 实际上,在“for each”函数中,您可以使用一个函数对象来克服困难,例如 struct printer { template<typename T> void operator()(const T& t) const { ... } };,因为传递一个“模板打印机”是行不通的。 - Algebraic Pavel
2
你可以制作一个将索引翻转的版本 - 从I=sizeof...(Tp)开始倒数计数。然后显式提供最大参数数目。你也可以制作一个基于标签类型中断的版本,比如break_t。然后,在你想停止打印时,在元组中放置一个该标签类型的对象。或者你可以在模板参数中提供一个停止类型。显然你不能在运行时中断。 - emsr
显示剩余10条评论

141
在 C++17 中,您可以使用 std::applyfold 表达式
std::apply([](auto&&... args) {((/* args.dosomething() */), ...);}, the_tuple);

打印元组的完整示例:

#include <tuple>
#include <iostream>

int main()
{
    std::tuple t{42, 'a', 4.2}; // Another C++17 feature: class template argument deduction
    std::apply([](auto&&... args) {((std::cout << args << '\n'), ...);}, t);
}

【在线示例】

此解决方案解决了M. Alaggan的答案中的评估顺序问题。


3
你能解释一下这里发生了什么吗:((std::cout << args << '\n'), ...);?lambda 表达式会将元组元素作为 args 展开并调用一次,但是双括号是怎么回事? - helmesjo
9
这里展开为逗号表达式 ((std::cout << arg1 << '\n'), (std::cout << arg2 << '\n'), (std::cout << arg3 << '\n')) - xskxzr
2
请注意,如果您想在逗号表达式中执行不合法的操作(例如声明变量和块),您可以将所有内容放入一个方法中,并从折叠逗号表达式内部调用它。 - Miral
@xskxzr 谢谢。那么,使用auto&&可以捕获rvalue和lvalue,但是auto&只能捕获lvalue?那么除非我要限制为只使用lvalue,否则auto&&总是更好的选择? - starriet
而且只需一个简单的快捷方式: #define FOR_EACH(tuple, callback) \ apply([](auto&&... args) { \ ((callback(args)), ...); \ }, tuple); - undefined
显示剩余2条评论

40

C++引入了扩展语句来实现这个目的。它们最初计划在C++20中推出,但由于缺乏语言措辞审查的时间而未能如期完成(请参见此处此处)。

目前已经确定的语法(请参见上面的链接)是:

{
    auto tup = std::make_tuple(0, 'a', 3.14);
    template for (auto elem : tup)
        std::cout << elem << std::endl;
}

1
Uuuuuuu. 我喜欢这个!! - Adrian
6
也错过了C++23并需要修订... https://github.com/cplusplus/papers/issues/156 - Chris

30

在C++17中,你可以这样做:

std::apply([](auto ...x){std::make_tuple(x.do_something()...);} , the_tuple);

在Clang++ 3.9中,使用std::experimental::apply已经可以运行。


7
这是否会导致迭代 - 即 do_something() 的调用 - 以未指定的顺序发生,因为参数包在函数调用 () 中展开,其中参数的顺序是未指定的?这可能非常重要;我想大多数人都希望按照成员索引的顺序(即作为 std::get<>() 的参数)保证顺序相同。据我所知,在这种情况下获得保证顺序,展开必须在 {花括号} 中完成。我错了吗?此答案强调了此类顺序: https://dev59.com/4WQo5IYBdhLWcg3wBLfy#16387374 - underscore_d
你如何获取元组元素的对应关系?相比于结构化绑定 for(auto [i, j], ... - gonidelis

30

Boost.Fusion 是一种可能性:

未经测试的示例:

struct DoSomething
{
    template<typename T>
    void operator()(T& t) const
    {
        t.do_sth();
    }
};

tuple<....> t = ...;
boost::fusion::for_each(t, DoSomething());

据我所知,它不会(至少在GCC 4.7.2上)?有任何提示的人吗? - sehe
@ViktorSehr 找到问题了:一个错误/遗漏导致融合行为取决于包含的顺序,请参见**Boost ticket #8418**获取更多详细信息。 - sehe
需要使用 boost::fusion::tuple 而不是 std::tuple 才能使其正常工作。 - Marcin
在GCC 8.1/mingw-64下,使用std lambda表达式的boost::fusion::for_each会出现两个警告:boost/mpl/assert.hpp:188:21: 警告:声明'assert_arg'时括号不必要[-Wparentheses] failed ************ (Pred::************boost/mpl/assert.hpp:193:21: 警告:声明'assert_not_arg'时括号不必要[-Wparentheses] failed ************ (boost::mpl::not_<Pred>::************ - Hossein

29

使用C++17中的if constexpr可以更简单、直观和编译器友好地完成这个任务:

// prints every element of a tuple
template<size_t I = 0, typename... Tp>
void print(std::tuple<Tp...>& t) {
    std::cout << std::get<I>(t) << " ";
    // do things
    if constexpr(I+1 != sizeof...(Tp))
        print<I+1>(t);
}

这是编译时递归,类似于@emsr提出的方法。但它不使用SFINAE,所以(我认为)更加符合编译器的要求。

24

使用 Boost.Hana 和泛型 lambda:

#include <tuple>
#include <iostream>
#include <boost/hana.hpp>
#include <boost/hana/ext/std/tuple.hpp>

struct Foo1 {
    int foo() const { return 42; }
};

struct Foo2 {
    int bar = 0;
    int foo() { bar = 24; return bar; }
};

int main() {
    using namespace std;
    using boost::hana::for_each;

    Foo1 foo1;
    Foo2 foo2;

    for_each(tie(foo1, foo2), [](auto &foo) {
        cout << foo.foo() << endl;
    });

    cout << "foo2.bar after mutation: " << foo2.bar << endl;
}

http://coliru.stacked-crooked.com/a/27b3691f55caf271


4
请不要使用命名空间boost::fusion(特别是与using namespace std一起使用)。现在无法知道for_eachstd::for_each还是boost::fusion::for_each - Bulletmagnet
4
@Bulletmagnet 这样做是为了简洁,在 ADL 中可以轻松处理。另外,这也是函数本地的。 - oblitum

12

这是一个简单的C++17方法,只使用标准库就可以迭代元组项:

#include <tuple>      // std::tuple
#include <functional> // std::invoke

template <
    size_t Index = 0, // start iteration at 0 index
    typename TTuple,  // the tuple type
    size_t Size =
        std::tuple_size_v<
            std::remove_reference_t<TTuple>>, // tuple size
    typename TCallable, // the callable to be invoked for each tuple item
    typename... TArgs   // other arguments to be passed to the callable 
>
void for_each(TTuple&& tuple, TCallable&& callable, TArgs&&... args)
{
    if constexpr (Index < Size)
    {
        std::invoke(callable, args..., std::get<Index>(tuple));

        if constexpr (Index + 1 < Size)
            for_each<Index + 1>(
                std::forward<TTuple>(tuple),
                std::forward<TCallable>(callable),
                std::forward<TArgs>(args)...);
    }
}

例子:

#include <iostream>

int main()
{
    std::tuple<int, char> items{1, 'a'};
    for_each(items, [](const auto& item) {
        std::cout << item << "\n";
    });
}

输出:

1
a

如果可调用对象返回一个值(但仍然可以使用不返回可分配布尔值的可调用对象,例如void),则可以将其扩展为有条件地中断循环:

#include <tuple>      // std::tuple
#include <functional> // std::invoke

template <
    size_t Index = 0, // start iteration at 0 index
    typename TTuple,  // the tuple type
    size_t Size =
    std::tuple_size_v<
    std::remove_reference_t<TTuple>>, // tuple size
    typename TCallable, // the callable to bo invoked for each tuple item
    typename... TArgs   // other arguments to be passed to the callable 
    >
    void for_each(TTuple&& tuple, TCallable&& callable, TArgs&&... args)
{
    if constexpr (Index < Size)
    {
        if constexpr (std::is_assignable_v<bool&, std::invoke_result_t<TCallable&&, TArgs&&..., decltype(std::get<Index>(tuple))>>)
        {
            if (!std::invoke(callable, args..., std::get<Index>(tuple)))
                return;
        }
        else
        {
            std::invoke(callable, args..., std::get<Index>(tuple));
        }

        if constexpr (Index + 1 < Size)
            for_each<Index + 1>(
                std::forward<TTuple>(tuple),
                std::forward<TCallable>(callable),
                std::forward<TArgs>(args)...);
    }
}

例子:

#include <iostream>

int main()
{
    std::tuple<int, char> items{ 1, 'a' };
    for_each(items, [](const auto& item) {
        std::cout << item << "\n";
    });

    std::cout << "---\n";

    for_each(items, [](const auto& item) {
        std::cout << item << "\n";
        return false;
    });
}

输出:

1
a
---
1

8

您需要使用模板元编程,这里展示了 Boost.Tuple 的用法:

#include <boost/tuple/tuple.hpp>
#include <iostream>

template <typename T_Tuple, size_t size>
struct print_tuple_helper {
    static std::ostream & print( std::ostream & s, const T_Tuple & t ) {
        return print_tuple_helper<T_Tuple,size-1>::print( s, t ) << boost::get<size-1>( t );
    }
};

template <typename T_Tuple>
struct print_tuple_helper<T_Tuple,0> {
    static std::ostream & print( std::ostream & s, const T_Tuple & ) {
        return s;
    }
};

template <typename T_Tuple>
std::ostream & print_tuple( std::ostream & s, const T_Tuple & t ) {
    return print_tuple_helper<T_Tuple,boost::tuples::length<T_Tuple>::value>::print( s, t );
}

int main() {

    const boost::tuple<int,char,float,char,double> t( 0, ' ', 2.5f, '\n', 3.1416 );
    print_tuple( std::cout, t );

    return 0;
}

在C++0x中,您可以将print_tuple()编写为变长模板函数。

8

首先定义一些索引帮助器:

template <size_t ...I>
struct index_sequence {};

template <size_t N, size_t ...I>
struct make_index_sequence : public make_index_sequence<N - 1, N - 1, I...> {};

template <size_t ...I>
struct make_index_sequence<0, I...> : public index_sequence<I...> {};

您想在每个元组元素上应用您的函数:

template <typename T>
/* ... */ foo(T t) { /* ... */ }

您可以编写:

template<typename ...T, size_t ...I>
/* ... */ do_foo_helper(std::tuple<T...> &ts, index_sequence<I...>) {
    std::tie(foo(std::get<I>(ts)) ...);
}

template <typename ...T>
/* ... */ do_foo(std::tuple<T...> &ts) {
    return do_foo_helper(ts, make_index_sequence<sizeof...(T)>());
}

如果foo返回void,请使用:

std::tie((foo(std::get<I>(ts)), 1) ... );

注意:在C++14中,make_index_sequence已经定义(http://en.cppreference.com/w/cpp/utility/integer_sequence)。
如果您需要左到右的求值顺序,请考虑以下内容:
template <typename T, typename ...R>
void do_foo_iter(T t, R ...r) {
    foo(t);
    do_foo(r...);
}

void do_foo_iter() {}

template<typename ...T, size_t ...I>
void do_foo_helper(std::tuple<T...> &ts, index_sequence<I...>) {
    do_foo_iter(std::get<I>(ts) ...);
}

template <typename ...T>
void do_foo(std::tuple<T...> &ts) {
    do_foo_helper(ts, make_index_sequence<sizeof...(T)>());
}

1
在调用 operator, 之前,应该将 foo 的返回值强制转换为 void,以避免可能出现的病态运算符重载。 - Yakk - Adam Nevraumont

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