author: hjjdebug
date: 2025年 05月 29日 星期四 11:42:41 CST
descrip: c++11 tuple概念和使用



tuple 是什么?
tuple 翻译过来是元组,至于tuple是什么,我们在使用中去体会,最后再给出概念.
原因是它结构很复杂,使用很简单.

0. 测试代码:

$ cat main.cpp
#include <tuple>

int main() {
    // 创建包含不同类型元素的元组
    std::tuple<int, double, const char *> person(10, 2.5, "Alice");
//    auto [i,f,s] = person; //这样的写法是最简单的. 自动绑定

    // 显式通过索引访问tuple元素
	auto i=std::get<0>(person);
	auto f=std::get<1>(person);
	auto s=std::get<2>(person);
	printf("i:%d,f:%f:s:%s\n",i,f,s);
	int size=std::tuple_size<decltype(person)>::value;
   	printf("sizeof(person):%ld,tuple_size:%d\n",sizeof(person),size); 
    return 0;
}

执行结果:
hjj@hjj-7090:~/test/tt$ ./tt
i:10,f:2.500000:s:Alice
sizeof(person):24,tuple_size:3

典型使用语句.

1. std::tuple<int, double, const char *> person(10, 2.5, “Alice”);

1.1. 可同时保存不同类型的数据

例如这里的, int,double,const char *;

1.2. 编译时确定大小

元组的大小和元素类型在编译时固定.
sizeof(person)给出了固定大小24=0x18 字节
剧透一下它的数据排布. 按说数据是封装起来的,不透露地址,不过若一定想知道…
还是通过汇编看出来的.
对象都有一个this指针, 指向数据块的起始地址.
const char * = this; //因为char *占用8bytes,所以
double = this +8; //因为double 占用 8bytes, 所以
int = this + 16; //因为int 占用4bytes, 但为了对齐给它8bytes,所以
//最终占用了24bytes

1.3. 声明了一个元组对象.

等价于声明了一种结构,但没有名称,可称之为匿名结构,
该结构虽然有类型,但却没有对应的成员名称. 这个被阉割的结构就是元组.
元组还实例化了一个对象.叫元组对象. 分别是 int 42; double 3.14; const char *;

结构中的成员变量的访问,通过成员名称访问.
元组中元素的访问,通过索引访问. 因为它没有成员名称. 只能说第一个元素,第二个元素…

访问一个对象中的某个元素,
最终计算机都是通过地址偏移得到被访者的地址, 找到地址后再找到类型,
结构体通过名称找到偏移, 元组通过索引找到偏移,
结构体通过名称找到类型,元组通过索引找到类型.
地址和类型都找到后,才能解引用找到数据, 其本质是一样的.

元组特别适合需要临时组合不同类型数据但又不想专门定义结构体的情景
就是临时把各种不同类型的数据组合起来.

2. tuple 使用如此简单

2.1 tuple 构建是如此简单: 把数据直接堆在一起就行

// 创建包含不同类型元素的元组
std::tuple<int, double, const char *> person(10, 2.5, "Alice");

401171: 48 8d 05 90 0e 00 00 lea 0xe90(%rip),%rax # 402008 "Alice"地址
401178: 48 89 45 e0 mov %rax,-0x20(%rbp) #保存地址到 -0x20(%rbp)
40117c: f2 0f 10 05 9c 0e 00 movsd 0xe9c(%rip),%xmm0 # 402020 ,程序中定义的2.5浮点数
401183: 00
401184: f2 0f 11 45 e8 movsd %xmm0,-0x18(%rbp) #保存浮点数2.5到 -0x18(%rbp)
401189: c7 45 f0 0a 00 00 00 movl $0xa,-0x10(%rbp) #保存10到 -0x10(%rbp)

2.2 tuple 数据获取如此简单,用get加索引

std::get<size_t I> 函数

// 显式通过索引访问tuple元素
auto i=std::get<0>(person);

401190: 48 8d 45 e0 lea -0x20(%rbp),%rax # this 指针 -0x20(%rbp) 给第以参数
401194: 48 89 c7 mov %rax,%rdi
401197: e8 6b 00 00 00 callq 401207 <std::tuple_element<0ul, std::tuple<int, double, char const*> >::type& std::get<0ul, int, double, char const*>(std::tuple<int, double, char const*>&)>
40119c: 8b 00 mov (%rax),%eax # 地址解解引用,得到数据
40119e: 89 45 cc mov %eax,-0x34(%rbp) # 保存到i -0x34(%rbp)

3. tuple 的底层实现很复杂, 不过我们可以不关心这个.

3.1 tuple get<size_t I,typename T> 的实现过程

实现是比较复杂的, 不是必需了解的. 不影响使用, 了解了更好.
看一下其调用栈:
0 in std::_Head_base<0ul, int, false>::_M_head of /usr/include/c++/9/tuple:160
1 in std::_Tuple_impl<0ul, int, double, char const*>::_M_head of /usr/include/c++/9/tuple:195
2 in std::__get_helper<0ul, int, double, char const*> of /usr/include/c++/9/tuple:1310
3 in std::get<0ul, int, double, char const*> of /usr/include/c++/9/tuple:1321
4 in main of main.cpp:9
关键就是找到元素的偏移. 它是通过4层封装才获得的.
我看了一下它的代码, 汇编码是看得懂的,基本上都是空调用, 但从源码上看却很复杂,因为各种类型名称已经非常长了.
举个例子. 获取0元素的第一层调用
0000000000401207 <std::tuple_element<0ul, std::tuple<int, double, char const*> >::type& std::get<0ul, int, double, char const*>(std::tuple<int, double, char const*>&)>:
{ return _Tuple_impl<__i, _Head, _Tail…>::_M_head(__t); }

/// Return a reference to the ith element of a tuple.
template<std::size_t __i, typename… _Elements>
constexpr __tuple_element_t<__i, tuple<_Elements…>>&
get(tuple<_Elements…>& __t) noexcept
401207: f3 0f 1e fa endbr64
40120b: 55 push %rbp
40120c: 48 89 e5 mov %rsp,%rbp
40120f: 48 83 ec 10 sub $0x10,%rsp
401213: 48 89 7d f8 mov %rdi,-0x8(%rbp)
{ return std::__get_helper<__i>(__t); }
401217: 48 8b 45 f8 mov -0x8(%rbp),%rax
40121b: 48 89 c7 mov %rax,%rdi
40121e: e8 02 00 00 00 callq 401225 <int& std::__get_helper<0ul, int, double, char const*>(std::_Tuple_impl<0ul, int, double, char const*>&)>
401223: c9 leaveq
401224: c3 retq

解释一下:
get 的模板参数类型是: “<0ur, int, double, char const*>”, 你见到过类型带逗号,带角标,的类型名称吗?
哦,这是被c++filt过滤后的名称,不管怎么说,模板的参数类型是一个由基本名加索引加各种基本类型组合得出的一个长类型名,保证其唯一性,正确性.
get 传递的对象类型是: “std::tuple<int,double,char const*>&”, 是tuple 模板类型
get 的返回类型是: <std::tuple_element<0ul, std::tuple<int, double, char const*> >::type& 返回的是std::tuple_element类模板中的type类型,
该类型名这么长, 被gcc推导后, 它就是 int &类型, 哪个长长的类型实际已被typedef 重定义为 int 了. 我靠!
这是不是跟当年的分式化简差不到,算到最后,分子分母一约分,复杂的东西全没了,得到了一个整数.

这些破玩意,在汇编看来都是没用的东西,都不会形成代码. 但对编译的安全性来说却很重要, gcc靠这些信息来保证各种数据操作类型是否匹配.
例如. 模板函数推断了其返回值是int, 你付给一个字符串对象, gcc 就会警告你了.

这4层代码最有用的就是第3层,_tuple_iml, 它在这里计算得到了元素的偏移地址, 第4层std::_Head_base 是一个空函数.
大体意思是第3层计算了base, 第4层用getbase() 函数调用得到base,因为是程序化框架,所以第4层就有点多余的成分.
那些反汇编代码就不贴了,核心就是上边几句了.

看一看tuple对象 person的结构:
//结构很复杂,使用很简单.
//gdb 中看到的person 对象
(gdb) p person
$1 = {
<std::_Tuple_impl<0, int, double, char const*>> = {
<std::_Tuple_impl<1, double, char const*>> = {
<std::_Tuple_impl<2, char const*>> = {
<std::_Head_base<2, char const*, false>> = {
_M_head_impl = 0x402008 “Alice”
}, },
<std::_Head_base<1, double, false>> = {
_M_head_impl = 2.5
}, },
<std::_Head_base<0, int, false>> = {
_M_head_impl = 10
}, }, }
//person 的 this 指针
(gdb) p &person
$2 = (std::tuple<int, double, char const*> *) 0x7fffffffdd20

4. tuple 的概念:

tuple 定义了一个有固定数目元素的容器,其中的每个元素类型都可以不相同.
tuple 是对pair的泛化.
td::tuple 是一个模板类,用于将多个值组合为一个单独的对象
tuple 允许程序员在单个变量中存储不同类型的元素。它类似于数组或结构体,
但每个元素可以是不同的类型,并且可以在编译时确定其内容和类型。
上边说的都没有错,都说了一种属性, 就像盲人摸象一样,都说的没错.

4.1 tuple 是一个支持各种类型的类模板.

它是这样定义的.
template<typename… _Elements>
class tuple;

4.2 : 它继承了 _Tuple_impl 实现类.

这个实现类我就不想再展开说了.
template<typename… _Elements>
class tuple : public _Tuple_impl<0, _Elements…>

4.3 tuple 的构造函数.

constexpr tuple(const _Elements&… __elements) : _Inherited(__elements…) { }
加constexpr 表示它是编译期函数.
_Elements&…, 表示它支持不定个数个参数.
它没做啥{},都让继承类去做了. _Inherited(__elements…)
tuple 还有copy构造,赋值构造,还有分配器,还有其它一堆一堆的构造,还有判别,符号重载,
1600行代码,我是看不懂了, 也不用看了.谁搞得这么复杂的玩意. 算了,能简单使用就可以了.

3. 小结.

tuple 的使用就是直接声明一个对象. 对象的类型由模板参数来确定,对象值由初始化列表来赋值.
使用上的简单对应着实现上的复杂.
这个对象,背后有一个隐含的,专有的tuple结构来支持. 似乎这个结构专为生成这一个对象而定义.
由于这个结构中没有定义成员变量,无法通过名称来访问, 但你可以通过索引来访问.

3.1 小插曲.

我忽然猜想, 是否一个正统的结构对象也能用索引的方式来访问呢?
答案是: 不行.
编译直接报错.
error: no matching function for call to ‘get<0>(MyStruct&)’
连编译这一关都不能通过,可见gcc还是很聪明的,这就是gcc的语法分析及类型匹配功能.
更何况, 就是gcc将来生成了代码让你这样用,你也不会这样用, 因为直接通过成员名称访问比你用索引访问,
更直接.更易读,更高效.

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐