C++输出任意tuple

  tuple是C++11标准中引入的一个模板类,用于将多个值组合成一个单一的对象,类似于一个固定大小的数组,但是每个元素可以是不同的类型。tuple可以接受任意数量、任意类型的值,并将它们组合成一个元组。tuple的使用非常方便,可以通过std::make_tuple函数创建一个元组。但是,也带来了一个问题,tuple中的参数类型和数量可以是任意的,那么想要输出一个tuple类型的数据就比较麻烦。本文记录一下常规输出tuple的方法和两种可以输出任意tuple类型数据的方法。
  使用C++可变参数模板类实现tuple可以参考博客的另一篇文章

常规输出tuple的方法

  一般情况下,如果我们创建了一个自己知道内容的tuple数据,我们可以通过std::get<i>的方法访问tuple中的元素,如下所示:

#include <iostream>
#include <tuple>

int main(int argc, char const *argv[])
{
    std::tuple<int, float, std::string> my_tuple(42, 3.14, "Hello, world!");
    std::cout << "(" 
              << std::get<0>(my_tuple) << ", "
              << std::get<1>(my_tuple) << ", "
              << std::get<2>(my_tuple) << ")"
              << std::endl;
    return 0;
}

  这样我们就可以得到我们想要的输出结果:

(42, 3.14, Hello, world!)

  这种方法仅限于我们知道tuple中有多少数据的时候,如果我们想用一种通用的方法输出任意长度和类型的tuple类时,使用get的方法就不再可行。下面将介绍两种方法用于输出任意类型的tuple。

递归和模板特化的方法

  我们可以使用递归模板和特化模板的方法来实现对任意类型tuple的输出,代码的实现如下:

#include <iostream>
#include <tuple>
using namespace std;

template <typename Tuple, std::size_t N>
struct tuple_printer {
    static void print(ostream& os, const Tuple& t) {
        tuple_printer<Tuple, N - 1>::print(os, t);
        os << ", " << std::get<N - 1>(t);
    }
};

template <typename Tuple>
struct tuple_printer<Tuple, 1> {
    static void print(ostream& os, const Tuple& t) { 
        os << std::get<0>(t); 
    }
};

template <typename... Args>
ostream& operator<<(ostream& os, const std::tuple<Args...>& t) {
    os << "(";
    tuple_printer<decltype(t), sizeof...(Args)>::print(os, t);
    os << ")";
    return os;
}

int main(int argc, char const* argv[]) {
    auto data = make_tuple("hello", 1, 2.5, 3.14159);
    cout << data;
    return 0;
}

  首先,我们需要定义一个tupel_printer类模板,它有两个模板参数:Tuple表示要输出的tuple类型,N表示tuple中元素的个数。tupel_printer中有一个静态成员函数print,该函数接受一个ostream&类型的参数和一个表示要输出的tuple对象的const引用参数。print函数使用递归模板来实现输出,首先调用tuple_printer<Tuple, N - 1>::print(os, t)来输出前N-1个元素,然后输出第N个元素,每个元素之间使用逗号隔开。接着,定义了一个特化版本的tupel_printer类模板用于处理递归的边界,即当N为1时,只输出tuple中唯一的一个元素。最后,定义一个重载的<<符号用于输出tuple类型的数据。
  上述代码的输出结果为:

(hello, 1, 2.5, 3.14159)

折叠表达式的方法

  C++17中引入一种新特性——折叠表达式(fold expression)。折叠表达式是一种用于简化模板元编程的语法特性,它允许将某个二元操作符(如逗号运算符,加法运算符等)应用于一个参数包中的所有元素,并将结果合并成一个值。如下所示:

#include <iostream>
using namespace std;

template<typename... Args>
auto sum(Args... args) {
    return (args + ...);
}

int main(int argc, char const *argv[])
{
    cout << sum(1,2,3,4,5);     // 输出15
    return 0;
}

  另外,也可以使用折叠表达式来打印一个参数包中所有的元素值:

#include <iostream>
using namespace std;

template<typename... Args>
void print(Args... args) {
    ((std::cout << args << " "), ...);
    std::cout << std::endl;
}

int main() {
    print(1, 2, 3, 4, 5);       // 输出 "1 2 3 4 5"
    return 0;
}

  折叠表达式可以和其他语法特性组合使用以实现对tuple的遍历,比如c++17中的apply或者C++14中的index_sequence

apply与折叠表达式

  此时,我们就可以使用c++17提供的std::apply方法结合折叠表达式实现对tuple类型数据的解包和输出,如下所示:

#include <iostream>
#include <tuple>
using namespace std;

template<typename... Args>
std::ostream& operator<<(std::ostream& os, const std::tuple<Args...>& t) {
    os << "(";
    std::apply([&os](const auto&... args) { ((os << args << ", "), ...); }, t);
    return os << ")";
}

int main(int argc, char const* argv[]) {
    auto data = make_tuple("hello", 1, 2.5, 3.14159);
    cout << data;
    return 0;
}

  上述代码的执行结果为:

(hello, 1, 2.5, 3.14159, )

  在上述的实现中,代码直接定义了一个重载<<运算符的模板函数,它接受一个std::ostream&类型的参数os和一个表示要输出的tuple对象的const引用参数。函数中,先输出一个左括号,然后使用std::apply函数来展开tuple中的所有元素,将lambda表达式应用到展开后的每一个元素上用于输出,元素之间用逗号隔开,最后输出一个右括号。因为使用了C++17的新特性,该方法相较于递归和模板特化的方法更加简洁,但是却不容易精准控制格式。

index_sequence与折叠表达式

  index_sequence_for可以生成参数包中对应的索引序列,使用index_sequence和折叠表达式就能够非常精准地控制格式,如下所示:

#include <iostream>
#include <tuple>
using namespace std;

template<typename Tuple, std::size_t... Is>
void printTuple(std::ostream& os,const Tuple& t, std::index_sequence<Is...>) {
    ((os << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}

template<typename... Args>
std::ostream& operator<<(std::ostream& os, const std::tuple<Args...>& t) {
    os << "(";
    printTuple(os, t, std::index_sequence_for<Args...>{});
    os << ")";
    return os;
}

int main(int argc, char const* argv[]) {
    auto data = make_tuple("hello", 1, 2.5, 3.14159);
    cout << data;
    return 0;
}

  上述代码的执行结果为:

(hello, 1, 2.5, 3.14159)

  在上述实现中,同样也是通过定义重载<<运算符的模板函数来实现输出的,不同的是调用了一个printTuple的模板函数,这个函数通过折叠表达式解包tuple数据的索引,实现对数据的访问和格式控制。这种方式即简洁也更容易实现对tuple数据的精准访问控制,效果相对比较好。


当珍惜每一片时光~