翩若惊鸿,矫若游龙。
Structured Binding,即“结构化绑定”,是C++17中引入的颇为强大的语法糖之一。
前言
阅读本章需要道友具备对右值引用、移动语义、基本模板编程的相关知识与较好理解基础。
由于C/C++只允许单个返回值,因而C/C++中需要从函数中“获取”多个值时,除了传递可变指针/引用所谓,利用最多的方法无疑就是返回一个OUT
变量(bushi)struct
或std::pair
/std::tuple
了:
1 | struct ret_info |
如此做的最大缺点之一就是访问每个成员不够便利——一个成员还能通过生命周期冲突百出的临时子成员访问,但访问多个成员时就必须另起一个变量接收之:
1 | auto st = func(); |
同时还可能涉及到 cv 修饰符差分、引用绑定等问题。
由此,C++17给出了 Structured Binding
这个极为强大的语法糖用于处理此类情景。
正题
首先明确 Structured Binding 的基本属性,一言具述之:
Like a reference, a structured binding is an alias to an existing object; unlike a reference,a structured binding does not have to be of a reference type.
即结构化绑定和引用相比,皆为既有对象的别名;但前者不一定是引用类型。
基本语法
Structured Binding的语法如下:
attr decl-specifier ref-qualifier [identifier-list] initializer ;
各部分解析:
- attr:任意数目的属性(attributes)
- decl-specifier:依下列顺序出现的修饰符(与变量声明规则一致):
static
、thread_local
、const
、volatile
、auto
;其中auto
必须出现
- ref-qualifier:可选
&
/&&
或置空
- [identifier-list]:变量名列表
- initializer:初始化表达式
显然其中重中之重的关键字无疑是
auto
——Structured Binding的类型不能手动指定,只允许使用auto
并以一定的修饰符约束之。
中间变量初始化
initializer
中的expr
可为任意表达式,与普通变量的初始化类似(除未加括号的逗号表达式,即
unphrenthesized comma expressions ):
= expr;
(expr);
{expr};
回顾结构化绑定的本质:其为一个既有对象的别名,初始化方式只有两种。首先引入一个隐式、不具名而唯一的变量e
(参考
lambda):
若 initializer 是一个数组类型且 ref-qualifier 置空,记 initializer 的类型为
cv1 A
,则定义e
为形如 attr decl-specifier(去除auto
)A e;
的新数组(即decl-specifierstd::remove_cvref_t<decltype(initializer)> e;
,此处不计属性);随后将 [identifier-list] 的各元素依 initializer 中的各元素复制/直接初始化(若为= expr
则为复制初始化,否则为直接初始化);否则,定义
e
形如 attr decl-specifier ref-qualifiere
initializer。
1 | const auto [a,b,c] = {1,2,3}; //the implicit `e` is `const std::initializer_list<int>` |
理解消化上述类型推导的代码实例对于理解、通晓 Structured Binding 的类型逻辑至关重要——建议各位道友自行举例并多加推导之。
应用
Structured Binding
中的变量有三种初始化方式,取决于右操作数的类型E
(严格地,std::remove_reference_t<decltype((e))>
):
E
是数组类型;
E
是一个非union
的class
/struct
,且std::tuple_size<E>
是具备数据成员value
的完整类型(complete type),无论std::tuple_size<E>::value
的变量类型、可访问性(accessibility)如何;
E
是一个非union
的class
/struct
,除情况2外的其它类型。
称情况(2)采用的是类元组绑定法(tuple-like binding);情况(3)则绑定至其可访问的数据成员上。
数组绑定法(binding to an array)
此种情况比较简单,即 Structured Binding
声明的每个变量名就是数组对应元素的别名(alias)。
由于这个数组是“虚拟”的中间数组,因此这些变量就是原始类型而非引用,但其底层存储的逻辑依然不变(连续存储,下同):
1 | int[] arr = {1,2,3}; |
类元组绑定法(tuple-like binding)
类元组绑定法的基础是存在成员std::tuple_size<E>::value
,所谓“实现了元组操作”(implemented
tuple operations)。
首先,std::tuple_size<E>::value
必须是良构(well-formed)的整型常量表达式,此时
Structured Binding 的大小与之一致。
即使
std::tuple_size<E>::value
不符合上述条件,但只要存在此成员(无论类型、可访问性),则编译器均会采用此方式进行绑定——纵使如此会令程序非良构(ill-formed)而不能通过编译。
对于 Structured Binding
中的第I
个数据成员,其类型为std::tuple_element<I,E>::type &&
,即底层类型为左值则为左值引用,否则为右值引用(参考
引用折叠/reference collaspe)。
其初始化表达式如下:
- 若类
E
存在名为get
的成员模板函数,且首个参数是常量模板参数(constant template parameter),则为e.get<I>()
;
- 否则为
std::get<I>(e)
,其中get
仅考虑ADL(Argument-Dependent Lookups)。
变量的生命周期与
e
一致,配合 lifetime extension 等保证了一般情况不会出现悬挂引用(dangaling reference)的情况。
同样地,此处的重载决议不会考虑
get
如此是否会令程序非良构,即使template<char*> void get();
亦会使编译器选择使用第一种“解读”方式进而编译错误。
仔细观察下列实例,必以求甚解为通晓之策:
1 | char ch{}; |
可见隐式“中间变量”e
的存在是关键:前缀无论存在各种
attributes 、cvref
修饰符,作用对象都是这个中间变量e
而非 Structured
Binding 自身引入的标识符。
因而可以发现即使是const auto
的结构化绑定,引入的变量亦可以是普通的引用类型。
数据成员绑定法(accessible data member binding)
此时 Structured Binding
的对象是e
的所有非静态数据成员,待绑定的变量数与其非静态数据成员的个数一致。
在声明结构化绑定的上下文(context)中,
e.name
语句必须是良构的(即必须满足可访问性限制等,如不可为非友元的private
)。
绑定后,各结构化绑定成员(即新引入的变量)都是该中间变量e
中各数据成员的别名(关于e
的各成员初始化见上文),初始化顺序为e
->各绑定成员(按声明顺序)。
划重点:本质上,每个绑定成员都是e
的各成员的左值别名,但这个别名不会呈现引用属性(虽然指代的是同一个对象)。考虑以下实例:
1 |
|
注释
Structured Binding 的类型必须被“自动”推导,不允许被 concepts 约束(constrained):
1 | template<class> |
有趣的是这里面涉及到的类模板实例化写法,亦是C++17引入的——C++17起类模板满足条件时允许依构造函数实参推导模板参数,如上例就会推导为
std::pair<int,int>
。
C++17前,由于只有函数模板才可以自动推导参数,因此历史遗留下来的std::make_pair
、std::make_tuple
等就是适配彼时语言机制的产物:
1
2
3
4
5
template<class Ty1,class Ty2>
std::pair<Ty1,Ty2> make_pair(Ty1 arg1,Ty2 arg2)
{
return std::pair<Ty1,Ty2>(arg1,arg2);
}
前行
C++20
C++17中lambda
不能捕获 Structured Binding
初始化的变量,C++20解除了此限制。
1 | int[] arr = {1,2}; |
C++26
C++26将新时代的“可变参数”概念进一步拓展,使 structured
binding 也获得了“可变”绑定的能力。
此前所述,接收并初始化的变量数必须和待绑定对象数一致;但C++26起允许将最后一个变量名以前置...
而变为一个可变参数包,其类型是一个std::tuple
,类型参数同样在编译期即被决定,其大小最小为0:
1 | int[] arr = {1,2,3}; |