如何更优雅地迭代 std::tuple ?
The following article is from CPP编程客 Author 里缪
std::tuple
属于异构数据类型结构,可以存储不同类型的数据。std::tuple
仍旧是个比较麻烦的事情。不过,随着C++元编程的发展,借助许多其他特性,暂时也能够稍微优雅地解决这个问题。01 C++11:原始迭代法
std::tuple
默认支持的访问方式为「索引式访问」,通过这种方式可以逐个访问结构中的元素。简单的例子:
1int main() {
2 std::tuple<int, double, const char*> tp {5, 5.19, "May 19"};
3
4 // 索引式访问
5 std::get<0>(tp) = 6;
6 std::cout << std::get<0>(tp) << '\n';
7 std::cout << std::get<1>(tp) << '\n';
8 std::cout << std::get<2>(tp) << '\n';
9}
此时若要遍历std::tuple
中的元素,重复工作很多,比较麻烦。
一种解决方法是借助可变参数模板,再通过递归来展开参数包,实现如下:
1template<typename Tuple>
2void print_tuple(const Tuple& tp) {
3}
4
5template<typename Tuple, std::size_t I, std::size_t...Is>
6void print_tuple(const Tuple& tp) {
7 std::cout << std::get<I>(tp) << '\n';
8 print_tuple<Tuple, Is...>(tp);
9}
现在,就可以使用该实现打印std::tuple
,如下:
print_tuple<decltype(tp), 0, 1, 2>(tp);
另一种解决方法是在一的基础上,借助逗号表达式和初始化列表来展开参数包,代码如下:
template<typename Tuple, std::size_t... Is>
void print_tuple(const Tuple& tp) {
std::initializer_list<int>{([&tp]() {
std::cout << std::get<Is>(tp) << '\n';
}(), 0)...};
}
02 C++14:std::index_sequence迭代法
到了C++14,标准中增加了std::index_sequence
用以表示索引序列,通过std::make_index_sequence
可以自动生成索引序列。
于是,借助这两个工具便可以替代C++11中的手动编写索引序列,使用起来更加方便。实现如下:
template<typename Tuple, std::size_t... Is>
void print_tuple(const Tuple& tp, std::index_sequence<Is...>) {
std::initializer_list<int>{([&tp]() {
std::cout << std::get<Is>(tp) << '\n';
}(), 0)...};
}
现在,可以这样使用:
std::tuple<int, double, const char*> tp {5, 5.19, "May 19"};
print_tuple(tp, std::make_index_sequence<3>{});
因为std::tuple
的元素个数能够通过std::tuple_size
获取,所以上述代码还可以进一步优化。代码如下:
template<typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& tp, std::index_sequence<Is...>) {
std::initializer_list<int>{([&tp]() {
std::cout << std::get<Is>(tp) << '\n';
}(), 0)...};
}
template<typename Tuple, std::size_t TupleSize = std::tuple_size<Tuple>::value>
void print_tuple(const Tuple& tp) {
print_tuple_impl(tp, std::make_index_sequence<TupleSize>{});
}
int main() {
std::tuple<int, double, const char*> tp {5, 5.19, "May 19"};
print_tuple(tp);
}
现在,虽说实现起来依然稍显麻烦,但使用起来已经比较方便了。
03 C++17:std::apply迭代法
到了C++17,则更进一步。
首先,拥有了Fold Expressions,展开参数包能够更加方便。
其次,增加了CTAD,使得std::tuple
的用法更加简洁,不用指定模板参数,可以自行推导。
最后,新添了std::apply
,该函数得以隐藏索引序列的具体使用,让代码更加简洁。
因此,现在可以更加优雅地解决上述问题,实现如下:
template<typename Tuple>
void print_tuple(const Tuple& tp) {
std::cout << "(";
if constexpr (std::tuple_size_v<Tuple> > 0)
std::apply(
[](const auto& head, const auto&... tails) {
std::cout << head;
((std::cout << ", " << tails), ...);
}, tp
);
std::cout << ")";
}
在此基础上,还可以重载operator <<
,使得可以直接通过输出流打印std::tuple
。
实现代码如下:
template<typename Tuple>
std::ostream& print_tuple(std::ostream& os, const Tuple& tp) {
os << "(";
if constexpr (std::tuple_size_v<Tuple> > 0)
std::apply(
[&os](const auto& head, const auto&... tails) {
os << head;
((os << ", " << tails), ...);
}, tp
);
std::cout << ")";
return os;
}
template<typename Tuple>
std::ostream& operator <<(std::ostream& os, const Tuple& tp) {
return print_tuple(os, tp);
}
现在,使用起来就非常方便了,测试前述例子,将会输出:
std::tuple tp {5, 5.19, "May 19"};
std::cout << tp; // output: (5, 5.19, May 19)
此外,欲编写一个针对std::tuple
的for_each
函数亦非难事,实现如下:
template<typename Tuple, typename Fn>
void for_each_tuple(Tuple&& tp, Fn&& fn) {
std::apply(
[&fn](auto&&... args) {
(fn(std::forward<decltype(args)>(args)), ...);
},
std::forward<Tuple>(tp)
);
}
int main() {
std::tuple tp {5, 5.19, "May 19"};
for_each_tuple(tp, [](auto&& v) {
std::cout << v << " ";
});
}
这个实现代码优雅,且威力强大,可以非常方便地操纵std::tuple
的每一个元素。
04 C++20:使用Template Lambda进一步优化
C++17实现版本的for_each_tuple
采用了generic lambda,在C++20可以使用template lambda消除其中的类型推导。
代码如下:
template<typename Tuple, typename Fn>
void for_each_tuple(Tuple&& tp, Fn&& fn) {
std::apply(
[&fn]<typename... T>(T&&... args) {
(fn(std::forward<T>(args)), ...);
},
std::forward<Tuple>(tp)
);
}
05 C++2?:Expansion Statements
上述各种实现,都颇有技巧性。
迭代这种异构数据类型结构的组件,提案中引入了Expansion Statements。可以直接这样编写代码:
auto tup = std::make_tuple(0, 'a', 3.14);
template for (auto& elem : tup)
std::cout << elem << '\n';
同样,这也是第三阶段元编程的重要工具之一,配合标准反射库,迭代复数式元函数返回的结果集。具体内容可以参考「反射主题」第一章和第四章。
此外,还有一个提案提出了扩展参数包的概念,根据该提案最终可以写出如下代码:
auto [...pack] = std::make_tuple(0, 'a', 3.14);
((std::cout << pack << '\n'), ...);
这里的pack便是一个「结构化绑定参数包」,可以在任何展开函数参数包的地方使用。
06 总结
由一个迭代std::tuple
的需求,就能带领大家领略C++11到未来的发展历程,发现需求痛点,以及新特性是如何解决这些痛点的。
同时,这也表现了C++元编程的发展历程,由晦涩的模板代码,到结构越发清晰的代码,再到第三阶段元编程的未来新特性,实现该需求越来越简单。
此外,大家可以发现,在第三阶段元编程的特性到来之前,C++17是可以相对优雅地实现该需求的最低C++版本。
希望大家能够理清本篇内容。如果你还有其他迷惑点,欢迎在评论区写下你的问题。
也欢迎给文章点赞、在看、分享。
- EOF -
加主页君微信,不仅C/C++技能+1
主页君日常还会在个人微信分享C/C++开发学习资源和技术文章精选,不定期分享一些有意思的活动、岗位内推以及如何用技术做业余项目
加个微信,打开一扇窗
关注『CPP开发者』
看精选C/C++技术文章
点赞和在看就是最大的支持❤️