
相关系列文章
C++之std::tuple(二) : 揭秘底层实现原理
C++三剑客之std::any(一) : 使用
C++之std::tuple(一) : 使用精讲(全)
C++三剑客之std::variant(一) : 使用
C++三剑客之std::variant(二):深入剖析
深入理解可变参数(va_list、std::initializer_list和可变参数模版)
std::apply源码分析
目录
1.简介
2.std::ignore介绍
3.创建元组
3.1.直接初始化方式
3.2.使用花括号初始化列表方式(C++11及以上版本)
3.3.make_tuple方式
3.4.使用std::tie()函数方式
4.元素访问
4.1.std::get()方式
4.2.使用结构化绑定(C++17及以上)
4.3.递归遍历元素
4.4.std::apply方式(C++17及以上)
5.获取std::tuple的size
6.获取元组中的元素类型
7.std::forward_as_tuple
8.std::tuple_cat
9.std::swap
10.std::make_from_tuple
11.项目实战
11.1.std::tuple的序列化和反序列化
11.2.获取std::tuple的数据大小
12.总结
C++11之后引入了std::tuple,俗称元组,元组(tuple)是一种用于组合多个不同类型的值的数据结构。元组可以将不同类型的数据打包在一起,类似于一个容器,可以按照索引顺序访问其中的元素。元组的大小在编译时确定,不支持动态添加或移除元素。std::tuple的定义如下:
template class tuple; std::tuple类似互C语言的结构体,不需要创建结构体而又有结构体的特征,在某些情况下可以取代结构体而使得程序更加简洁,直观。std::tuple理论上可以定义无数多个不同类型的成员变量。特别是你需要在函数之间返回多个值时,或者需要一次性处理多个相关值时,使用元组可以简化代码并提高可读性。
在标头 定义,任何值均可赋给而无效果的未指定类型的对象。目的是令 std::tie 在解包 std::tuple 时作为不使用的参数的占位符使用。例如:解包 set.insert() 所返回的 pair ,但只保存布尔值。
#include #include #include #include int main() { std::set set_of_str; bool inserted = false; std::tie(std::ignore, inserted) = set_of_str.insert("Test"); if (inserted) { std::cout << "Value was inserted successfully\n"; } } 输出:Value was inserted successfully
//显示初始化 std::tuple a(true, 1, 3.0, "1112222"); //显示初始化 std::tuple a{true, 1, 3.0, "1112222"}; //显示初始化 std::tuple a = make_tuple(true, 1, 3.0, "1112222"); //隐式初始化 auto b = make_tuple(true, 1, 3.0, "1112222"); std::tie定义为:
template constexpr tuple tie (Types&... args) noexcept; std::tie生成一个tuple,此tuple包含的分量全部为实参的引用,与make_tuple完全相反。主要用于从tuple中提取数据。例如:
bool myBool; int myInt; double myDouble; std::string myString; std::tie(myBool, myInt, myDouble, myString) = std::make_tuple(true, 1, 3.0, "1112222"); 如果是要忽略某个特定的元素,还可以使用第2章节的std::ignore来占位,例如:
bool myBool; std::string myString; std::tie(myBool, std::ignore, std::ignore, myString) = std::make_tuple(true, 1, 3.0, "1112222"); 使用std::get来访问std::tuple特定的元素,如:
std::tuple a(true, 0, "sfsfs"); bool b = std::get<0>(a); int c = std::get<1>(a); std::string d = std::get<2>(a); std::get<0>(a) = false; std::get<2>(a) = "s344242"; 在C++17及以上版本中,还可以使用结构化绑定 (structured bindings) 的方式来创建和访问元组,可以更方便地访问和操作元组中的元素。结构化绑定允许直接从元组中提取元素并赋值给相应的变量。例如:
std::tuple myTuple(true, false, "Hello"); auto [a, b, c] = myTuple; 这将自动创建变量a、b和c,并将元组中相应位置的值赋给它们。
注意:
元组是不可变的(immutable)一旦创建就不能更改其元素的值。但是,可以通过解构赋值或使用std::get
(tuple)来获取元组中的值,并将新的值赋给它们,从而修改元组中的值。
std::tuple不支持迭代器,获取元素的值时只能通过元素索引或tie解包。给定的索引必须是在编译期间就已经确定的,不能在运行期间动态传递,否则会产生编译错误
由于 tuple 自身的原因,无法直接遍历,而 get
所以 tuple 的遍历需要我们手写,代码如下:
template struct VisitTuple { static void Visit(const Tuple& value) { VisitTuple::Visit(value); std::cout << ' ' << std::get(value); return void(); } }; template struct VisitTuple { static void Visit(const Tuple& value) { std::cout << std::get<0>(value); return void(); } }; template void TupleVisit(const std::tuple& value) { VisitTuple::Visit(value); } 利用可变参数的折叠表达式规则来访问std::tuple的元素,例如:
#include #include #include int add(int first, int second) { return first + second; } template T add_generic(T first, T second) { return first + second; } auto add_lambda = [](auto first, auto second) { return first + second; }; template std::ostream& operator<<(std::ostream& os, std::tuple const& theTuple) { std::apply ( [&os](Ts const&... tupleArgs) { os << '['; std::size_t n{0}; ((os << tupleArgs << (++n != sizeof...(Ts) ? ", " : "")), ...); os << ']'; }, theTuple ); return os; } int main() { // OK std::cout << std::apply(add, std::pair(1, 2)) << '\n'; // 错误:无法推导函数类型 // std::cout << std::apply(add_generic, std::make_pair(2.0f, 3.0f)) << '\n'; // OK std::cout << std::apply(add_lambda, std::pair(2.0f, 3.0f)) << '\n'; // 进阶示例 std::tuple myTuple(25, "Hello", 9.31f, 'c'); std::cout << myTuple << '\n'; } 输出:
3 5 [25, Hello, 9.31, c] 上面语句((os << tupleArgs << (++n != sizeof...(Ts) ? ", " : "")), ...);利用了C++17的折叠表达式,折叠表达式是C++17新引进的语法特性。使用折叠表达式可以简化对C++11中引入的参数包的处理,从而在某些情况下避免使用递归。如果有不是很明白的地方,可参考我的博客深入理解可变参数(va_list、std::initializer_list和可变参数模版)-CSDN博客
关于std::applay的使用有不明白的地方,可以参考我的博客std::apply源码分析-CSDN博客
std::tuple_size的定义如下:
template< class... Types > struct tuple_size< std::tuple > : std::integral_constant { }; 提供对 tuple 中元素数量的访问,作为编译时常量表达式,计算std::tuple的大小。例如:
#include #include template void test(T value) { int a[std::tuple_size_v]; // 能用于编译时 std::cout << std::tuple_size{} << ' ' // 或运行时 << sizeof a << ' ' << sizeof value << '\n'; } int main() { test(std::make_tuple(1, 2, 3.14)); } 可能的输出:3 12 16
std::tuple_element定义如下:
template< std::size_t I, class... Types > class tuple_element< I, tuple >; 可以使用std::tuple_element
#include #include template struct type_list { template using type = typename std::tuple_element>::type; }; int main() { std::cout << std::boolalpha; type_list::type<2> x = true; std::cout << x << '\n'; } 输出:true
定义如下:
template< class... Types > tuple forward_as_tuple( Types&&... args ) noexcept; template< class... Types > constexpr tuple forward_as_tuple( Types&&... args ) noexcept; 用于接受右值引用数据生成 tuple, 与 std::make_tuple 不同的是它的右值是引用的,当修改其值的时候,原来赋值所用的右值也将修改,实质上就是赋予了它地址。同std::tie一样,也是生成一个全是引用的tuple,不过std::tie只接受左值,而std::forward_as_tuple左值、右值都接受。主要是用于不损失类型属性的转发数据。
注意此处 tuple 内的类型应为引用,否则相当于 std::make_tuple。例如:
signed main(int argc, char *argv[]) { int a = 123, c = 456; float b = 33.f, d = .155; std::tuple tu = std::forward_as_tuple(a,b,c,d); std::get<0> (tu) = 2; std::get<1> (tu) = 4.5f; std::get<2> (tu) = 234; std::get<3> (tu) = 22.f; std::cout << a << std::endl; // 2 std::cout << b << std::endl; // 4.5 std::cout << c << std::endl; // 234 std::cout << d << std::endl; // 22 return 0; } 注意:若参数是临时量,则
forward_as_tuple不延续其生存期;必须在完整表达式结尾前使用它们。
此函数接受多个tuple作为参数,然后返回一个tuple。返回的这个tuple将tuple_cat的参数中的tuple的所有元素按所属的tuple在参数中的顺序以及其在tuple中的顺序排列成一个新的tuple。新tuple中元素的类型与参数中的tuple中的元素的类型完全一致。例如:
#include #include #include // 打印任何大小 tuple 的辅助函数 template struct TuplePrinter { static void print(const Tuple& t) { TuplePrinter::print(t); std::cout << ", " << std::get(t); } }; template struct TuplePrinter { static void print(const Tuple& t) { std::cout << std::get<0>(t); } }; template void print(const std::tuple& t) { std::cout << "("; TuplePrinter::print(t); std::cout << ")\n"; } // 辅助函数结束 int main() { std::tuple t1(10, "Test", 3.14); int n = 7; auto t2 = std::tuple_cat(t1, std::make_tuple("Foo", "bar"), t1, std::tie(n)); n = 10; print(t2); } 输出:(10, Test, 3.14, Foo, bar, 10, Test, 3.14, 10)
交换两个std::tuple的内容,前提是两个std::tuple的大小和元素类型必须相同,例如:
std::tuple a1; std::tuple a2; std::tuple a3; std::tuple a4; std::tuple a5; a1.swap(a2); //OK a2.swap(a3); //编译出现error a3.swap(a4);//编译出现error a4.swap(a5);//编译出现error 上面a1和a2的大小和元素类型都相同,因此可以交换。a2和a3、a3和a4、a4和a5类型不相同,因此不能交换。我们再看一个std::tuple交换的例子:
#include #include #include int main() { std::tuple p1{42, "ABCD", 2.71}, p2; p2 = std::make_tuple(10, "1234", 3.14); auto print_p1_p2 = [&](auto rem) { std::cout << rem << "p1 = {" << std::get<0>(p1) << ", " << std::get<1>(p1) << ", " << std::get<2>(p1) << "}, " << "p2 = {" << std::get<0>(p2) << ", " << std::get<1>(p2) << ", " << std::get<2>(p2) << "}\n"; }; print_p1_p2("Before p1.swap(p2): "); p1.swap(p2); print_p1_p2("After p1.swap(p2): "); swap(p1, p2); print_p1_p2("After swap(p1, p2): "); } 输出:
Before p1.swap(p2): p1 = {42, ABCD, 2.71}, p2 = {10, 1234, 3.14} After p1.swap(p2): p1 = {10, 1234, 3.14}, p2 = {42, ABCD, 2.71} After swap(p1, p2): p1 = {42, ABCD, 2.71}, p2 = {10, 1234, 3.14} std::make_from_tuple是以元组std::tuple的元素作为构造函数的参数构造别的类型对象,如下例子:
#include #include struct Foo { Foo(int first, float second, int third) { std::cout << first << ", " << second << ", " << third << "\n"; } }; int main() { auto tuple = std::make_tuple(42, 3.14f, 0); std::make_from_tuple(std::move(tuple)); } 输出:42, 3.14, 0
利用QDataStream的序列化数据,重写操作符operator<<和operator>>,代码如下:
template QDataStream& operator<<(QDataStream& dataStream, std::tuple const& theTuple) { std::apply ( [&dataStream](Ts const&... tupleArgs){ ((dataStream << tupleArgs), ...); }, theTuple ); return dataStream; } template QDataStream& operator>>(QDataStream& dataStream, std::tuple& theTuple) { std::apply ( [&dataStream](Ts&... tupleArgs){ ((dataStream >> tupleArgs), ...); }, theTuple ); return dataStream; } 或 template QDataStream& operator<<(QDataStream& dataStream, std::tuple const& theTuple) { std::apply ( [&dataStream](Ts const&... tupleArgs){ (dataStream << ... << tupleArgs); }, theTuple ); return dataStream; } template QDataStream& operator>>(QDataStream& dataStream, std::tuple& theTuple) { std::apply ( [&dataStream](Ts&... tupleArgs){ (dataStream >> ... >> tupleArgs); }, theTuple ); return dataStream; } 获取std::tuple的实际内容大小,但是std::tuple不能包含可变内容长度字段,代码如下:
template struct stTupleContentSize{ using first = typename std::tuple_element::type; using others = stTupleContentSize; static constexpr std::size_t = sizeof(first) + others::size; }; template struct stTupleContentSize{ using first = typename std::tuple_element<0, Tuple>::type; static constexpr std::size_t = sizeof(first); }; 调用方法:
using queryWaveParamData = std::tuple; constexpr int queryWaveParamDataSize = stTupleContentSize>::size; std::tuple 是一种重要的数据结构,可以用于在函数参数之间传递数据,也可以作为函数的返回值。在实际项目中,我们可以灵活地使用 std::tuple,以简化代码,提高程序的性能。
后面我们将继续通过分析std::tuple源码的方式来更深层次讲解它的实现原理,值得期待哦。。。
参考:std::tuple - cppreference.com