对于一个标准的 c++容器来说,我们可以很容易在运行时使用迭代器和 range-based for loop 来遍历其中的每一个元素。但是对于std::tuple,却不能像普通的容器那样去遍历它。

std::tuple 简介

std::tuple是一个具有固定大小,包含不同类型值的集和。与之相似的是std::pair,只不过std::pair只能容纳两个元素, 而std::tuple可以容纳许多元素:

1std::tuple<int, double, const char*> tup {42, 10.5, "hello"};
2
3// or with CTAD(class template argument deduction), C++17:
4std::tuple deducedTup {42, 10.5, "hello"}; // 自动推导类型

访问std::pair中的元素只需要访问.first.second成员即可:

1std::pair p = {10, 10.5};
2p.first += 10;

然而std::tuple并没有类似于.first .second的成员,标准库中我们只能通过std::get来直接访问其中的元素:

 1std::tuple tp {1, 2, "hello", "world", 10.5};
 2
 3// by index
 4std::get<0>(tp) += 10;
 5std::get<1>(tp) *= 20;
 6std::cout << std::get<2>(tp) << '\n'; // hello
 7std::cout << std::get<3>(tp) << '\n'; // world
 8
 9// by type
10std::cout << std::get<double>(tp) << '\n'; // 10.5

如何遍历 tuple

对 tuple 有了初步的了解,也知道了如何去获取 tuple 中的元素,那么接下来我们就来一步步构造对 tuple 遍历的方法。

准备工作

首先我们来编写下面这样一段代码,通过向模板中传递一系列想要访问的元素下标,来完成对tuple的遍历:

 1template<typename T> void print(const T& t)
 2{
 3    std::cout << t << ',';
 4}
 5
 6template<typename TupleT, std::size_t... Is> void print_tuple(const TupleT& tp)
 7{
 8    (print(std::get<Is>(tp)), ...);
 9}
10
11int main(int argc, char* argv[])
12{
13    std::tuple tp{1, 2, "hello", "world"};
14    print_tuple<decltype(tp), 0, 1, 2, 3>(tp);
15    return 0;
16}

代码比较简单,模板参数除了tuple类型以外,还需要传递一组"non-type"模板参数,也就是指定要访问的元素下标, 然后通过 c++17 的 fold expression 来展开调用print方法,打印对应下标的值。

使用 index_sequence 优化

在上面的代码中,我们需要手动传递元素的下标,那么有没有一种更好的办法,让编译器自动为我们生成元素下标,从而简化调用呢? 答案是可以的。在 c++14 中引入了std::index_sequencestd::make_index_sequence,这样的话我们的程序可以做以下优化:

 1template<typename T> void print(const T& t)
 2{
 3    std::cout << t << ',';
 4}
 5
 6template<typename TupleT, std::size_t... Is>
 7void print_tuple(const TupleT& tp, std::index_sequence<Is...>)
 8{
 9    (print(std::get<Is>(tp)), ...);
10}
11
12
13int main(int argc, char* argv[])
14{
15    std::tuple tp{1, 2, "hello", "world"};
16    print_tuple(tp, std::make_index_sequence<4>());
17    return 0;
18}

以上代码通过std::make_index_sequencestd::index_sequence在编译期自动生成了元素下标的"non-type"模板参数, 但是在调用的时候还需要手动传 tupe 的 size,因此我们使用std::tuple_size来继续对代码进行优化:

 1template<typename T> void print(const T& t)
 2{
 3    std::cout << t << ',';
 4}
 5
 6template<typename TupleT, std::size_t... Is>
 7void print_tuple(const TupleT& tp, std::index_sequence<Is...>)
 8{
 9    (print(std::get<Is>(tp)), ...);
10}
11
12
13int main(int argc, char* argv[])
14{
15    std::tuple tp{1, 2, "hello", "world"};
16    print_tuple(tp, std::make_index_sequence<std::tupe_size_v<decltype(tp)>>());
17    return 0;
18}

接下来我们再对代码进行一些封装,从而让我们的使用再简单一些:

 1template<typename T> void print(const T& t)
 2{
 3    std::cout << t << ',';
 4}
 5
 6template<typename TupleT, std::size_t... Is>
 7void print_tuple_manual(const TupleT& tp, std::index_sequence<Is...>)
 8{
 9    (print(std::get<Is>(tp)), ...);
10}
11
12template<typename TupleT, std::size_t tuple_size = std::tuple_size_v<TupleT>>
13void print_tuple(const TupleT& tp)
14{
15    print_tuple_manual(tp, std::make_index_sequence<tuple_size>());
16}
17
18int main(int argc, char* argv[])
19{
20    std::tuple tp{1, 2, "hello", "world"};
21    print_tuple(tp);
22    return 0;
23}

打印 tuple

虽然我们已经能够简单的向print_tuple函数传递一个tuple实例来实现对tuple内元素的打印了,但是 我们还是希望对打印的格式进行一个优化,下面我们来为打印加上括号和下标:

 1template<typename TupleT, std::size_t... Is>
 2void print_tuple_manual(const TupleT& tp, std::index_sequence<Is...>)
 3{
 4    auto print = [](const auto& x, std::size_t idx) {
 5        if (idx > 0) {
 6            std::cout << ", ";
 7        }
 8        std::cout << idx << ": " << x;
 9    };
10    std::cout << '(';
11    (print(std::get<Is>(tp), Is), ...);
12    std::cout << ')';
13}
14
15template<typename TupleT, std::size_t tuple_size = std::tuple_size_v<TupleT>>
16void print_tuple(const TupleT& tp)
17{
18    print_tuple_manual(tp, std::make_index_sequence<tuple_size>());
19}
20
21int main(int argc, char* argv[])
22{
23    std::tuple tp{1, 2, "hello", "world"};
24    print_tuple(tp); // (0: 1, 1: 2, 2: hello, 3: world)
25    return 0;
26}

\<\<操作符

下面我们继续优化,为std::tuple实现<<操作符:

 1template<typename TupleT, std::size_t... Is>
 2std::ostream& print_tuple_manual(std::ostream& os, const TupleT& tp, std::index_sequence<Is...>)
 3{
 4    auto print = [&os](const auto& x, std::size_t idx) {
 5        if (idx > 0) {
 6            os << ", ";
 7        }
 8        os << idx << ": " << x;
 9    };
10    os << '(';
11    (print(std::get<Is>(tp), Is), ...);
12    os << ')';
13    return os;
14}
15
16template<typename TupleT, std::size_t tuple_size = std::tuple_size<TupleT>::value>
17std::ostream& operator<<(std::ostream& os, const TupleT& tp)
18{
19    return print_tuple_manual(os, tp, std::make_index_sequence<tuple_size>{});
20}
21
22int main(int argc, char* argv[])
23{
24    std::tuple tp{1, 2, "hello", "world"};
25    std::cout << tp << std::endl; // (0: 1, 1: 2, 2: hello, 3: world)
26    return 0;
27}