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

std::tuple 简介

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

std::tuple<int, double, const char*> tup {42, 10.5, "hello"};

// or with CTAD(class template argument deduction), C++17:
std::tuple deducedTup {42, 10.5, "hello"}; // 自动推导类型

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

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

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

std::tuple tp {1, 2, "hello", "world", 10.5};

// by index
std::get<0>(tp) += 10;
std::get<1>(tp) *= 20;
std::cout << std::get<2>(tp) << '\n'; // hello
std::cout << std::get<3>(tp) << '\n'; // world

// by type
std::cout << std::get<double>(tp) << '\n'; // 10.5

如何遍历 tuple

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

准备工作

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

template<typename T> void print(const T& t)
{
    std::cout << t << ',';
}

template<typename TupleT, std::size_t... Is> void print_tuple(const TupleT& tp)
{
    (print(std::get<Is>(tp)), ...);
}

int main(int argc, char* argv[])
{
    std::tuple tp{1, 2, "hello", "world"};
    print_tuple<decltype(tp), 0, 1, 2, 3>(tp);
    return 0;
}

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

使用 index_sequence 优化

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

template<typename T> void print(const T& t)
{
    std::cout << t << ',';
}

template<typename TupleT, std::size_t... Is>
void print_tuple(const TupleT& tp, std::index_sequence<Is...>)
{
    (print(std::get<Is>(tp)), ...);
}


int main(int argc, char* argv[])
{
    std::tuple tp{1, 2, "hello", "world"};
    print_tuple(tp, std::make_index_sequence<4>());
    return 0;
}

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

template<typename T> void print(const T& t)
{
    std::cout << t << ',';
}

template<typename TupleT, std::size_t... Is>
void print_tuple(const TupleT& tp, std::index_sequence<Is...>)
{
    (print(std::get<Is>(tp)), ...);
}


int main(int argc, char* argv[])
{
    std::tuple tp{1, 2, "hello", "world"};
    print_tuple(tp, std::make_index_sequence<std::tupe_size_v<decltype(tp)>>());
    return 0;
}

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

template<typename T> void print(const T& t)
{
    std::cout << t << ',';
}

template<typename TupleT, std::size_t... Is>
void print_tuple_manual(const TupleT& tp, std::index_sequence<Is...>)
{
    (print(std::get<Is>(tp)), ...);
}

template<typename TupleT, std::size_t tuple_size = std::tuple_size_v<TupleT>>
void print_tuple(const TupleT& tp)
{
    print_tuple_manual(tp, std::make_index_sequence<tuple_size>());
}

int main(int argc, char* argv[])
{
    std::tuple tp{1, 2, "hello", "world"};
    print_tuple(tp);
    return 0;
}

打印 tuple

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

template<typename TupleT, std::size_t... Is>
void print_tuple_manual(const TupleT& tp, std::index_sequence<Is...>)
{
    auto print = [](const auto& x, std::size_t idx) {
        if (idx > 0) {
            std::cout << ", ";
        }
        std::cout << idx << ": " << x;
    };
    std::cout << '(';
    (print(std::get<Is>(tp), Is), ...);
    std::cout << ')';
}

template<typename TupleT, std::size_t tuple_size = std::tuple_size_v<TupleT>>
void print_tuple(const TupleT& tp)
{
    print_tuple_manual(tp, std::make_index_sequence<tuple_size>());
}

int main(int argc, char* argv[])
{
    std::tuple tp{1, 2, "hello", "world"};
    print_tuple(tp); // (0: 1, 1: 2, 2: hello, 3: world)
    return 0;
}

\<\<操作符

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

template<typename TupleT, std::size_t... Is>
std::ostream& print_tuple_manual(std::ostream& os, const TupleT& tp, std::index_sequence<Is...>)
{
    auto print = [&os](const auto& x, std::size_t idx) {
        if (idx > 0) {
            os << ", ";
        }
        os << idx << ": " << x;
    };
    os << '(';
    (print(std::get<Is>(tp), Is), ...);
    os << ')';
    return os;
}

template<typename TupleT, std::size_t tuple_size = std::tuple_size<TupleT>::value>
std::ostream& operator<<(std::ostream& os, const TupleT& tp)
{
    return print_tuple_manual(os, tp, std::make_index_sequence<tuple_size>{});
}

int main(int argc, char* argv[])
{
    std::tuple tp{1, 2, "hello", "world"};
    std::cout << tp << std::endl; // (0: 1, 1: 2, 2: hello, 3: world)
    return 0;
}