C++17结构化绑定
600 Words | Read in about 3 Min | View times
Overview
C++17提供了结构化绑定机制,可以使用指定名称绑定到初始化的子对象或元素上,与引用绑定别名类似,但结构化绑定的类型不需要是引用类型。得益于自动推导技术越来越成熟,通过auto
声明的多个变量绑定到一个复杂结构成了可能。本节内容我们来介绍C++17的新特性——结构化绑定。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
引子
回忆一下std::map
的insert
函数的声明原型:
1std::pair<iterator, bool> insert(const value_type& value);
2
3template<class P>
4std::pair<iterator, bool> insert(P&& value);
5
6std::pair<iterator, bool> insert(value_type&& value);
插入方法的返回值是一个std::pari<iterator, bool>
,那么使用这个返回值是就需要大量的first
和second
,繁琐且可读性比较差:
1#include <iostream>
2#include <map>
3
4int main() {
5 std::map<int, int> mp;
6 std::pair<std::map<int, int>::iterator, bool> result = mp.insert({1, 1});
7 if (result.second)
8 std::cout << "inserted" << std::endl;
9
10 for (auto itr = mp.begin(); itr != mp.end(); ++itr) {
11 std::cout << "{" << itr->first << ", " << itr->second << "}" << std::endl;
12 }
13
14 return 0;
15}
为了改善这些问题,C++11引入了std::tie()
,定义在标准库头文件<tuple>
中:
1template<class... Types>
2std::tuple<Types&...> tie(Types&... args) noexcept;
构造一个到其参数或std::ignore
实例的左值引用的std::tuple
对象,其中std::ignore
是一个占位符,其所在位置的赋值被忽略,这样既可以明确返回的参数含义,又可以提高可读性:
1#include <iostream>
2#include <map>
3#include <tuple>
4
5int main() {
6 std::map<int, int> mp;
7 bool inserted;
8 std::tie(std::ignore, inserted) = mp.insert({1, 1});
9 if (inserted)
10 std::cout << "inserted" << std::endl;
11
12 return 0;
13}
从这个例子可以看到,insert()
返回的std::pair
的first
元素被绑定在std::ignore
上,即忽略赋值,而second
元素被绑定在inserted
变量上,如此一来可以非常明确出second
元素的含义代表着是否插入成功,大大提高了代码的可读性。
然而,std::tie()
也有明显的缺点:
-
绑定的变量必须提前声明,且其类型必须提前明确,不能自动推导
-
由于绑定的变量需要提前声明和定义,对于复杂对象会存在一次默认构造的过程,然后才被绑定赋值为新的数值,这一步初始化过程是冗余的
-
无法应用于没有默认构造函数的场景
这时候,C++17的结构化绑定就应运而生了。
什么是结构化绑定
结构化绑定,Structured Bindings,可以对数组array、元组tuple、结构体struct的成员变量进行绑定。类似引用,结构化绑定是一个已存在对象的别名。不同于引用的是,结构化绑定的类型不必为引用类型。
结构化绑定的3种基本语法如下:
1attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] = expression;
2attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] { expression };
3attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] ( expression );
其实可以统一成一种表达形式:
1attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] initializer
各个部分的说明如下:
-
attr
:可选的属性序列 -
cv-auto
:可以是被const
或volatile
修饰的auto
类型,也可以包含static
或thread_local
,也可以都没有 -
ref-qualifier
:&
或&&
-
identifier-list
:用逗号分隔的标识符名称的列表 -
initializer
:包括= expression
、{expression}
和(expression)
三种表达形式。
结构化绑定声明将identifier-loist
中的所有标识符引入作为其外围作用域中的名字,并将它们绑定到expression
所指代的对象的各个子对象或元素,这种绑定被称作结构化绑定。
我们先简单示范一下它的用法,将前面的例子用结构化绑定的写法改造一下:
1#include <iostream>
2#include <map>
3
4int main()
5{
6 std::map<int, int> map;
7 auto&& [itr, inserted] = map.insert({ 1, 2 });
8 if (inserted)
9 std::cout << "inserted" << std::endl;
10
11 for (auto&& [k, v] : map)
12 std::cout << "{" << k << ", " << v << "}" << std::endl;
13}
接下来我们详细讲述一下结构化绑定的实现过程。
结构化绑定的实现过程
结构化绑定声明首先引入一个类型为E的唯一命名变量e,来持有其initializer
表达式的值。其中E = std::remove_reference_t<decltype((expression))>
。
绑定规则如下:
-
如果
expression
是数组类型A
,且不存在ref-qualifier
,则E为cv-A
,其中cv
是cv-auto
中的cv
限定符,且e中的各个元素从expression
的对应元素进行拷贝初始化或直接初始化; -
否则相当于定义e为
attr cv-auto ref-qualifier e initializer
,e即代表着[ identifier-list ]
。此时attr cv-auto ref-qualifier
都是修饰e
的,而不是那些新声明的变量。
独立出第一条规则是因为C++不允许第二条规则作用于数组。
结构化绑定可以使用以下三种方式进行绑定来决定E:
-
如果E是数组类型,则绑定各个名字到数组的各个元素。即绑定数组。
-
如果E不是
union
类型,且std::tuple_size<E>
是完整类型且拥有名为value
的成员,则使用类元组的绑定方式。即绑定类元组。 -
如果E不是
union
类型,且std::tuple_size<E>
不是完整类型,则绑定到E的各个可访问的数据成员上。即绑定结构体。
注意,其中std::tuple_size<E>
是提供编译时判断,如果E
是std::tuple
、std::array
、std::pair
这些类元组类型,那么std::tuple_size<E>
就是一个完整类型,否则就不是完整类型,该方法是用来在编译时判断是否为类元组类型。
如果是完整类型,那么std::tuple_size<E>::value
就是合法的定义,代表该类元组数据的元素个数。
Each structured binding has a referenced type, defined in the description below. This type is the type returned by decltype when applied to an unparenthesized structured binding.
每一个结构化绑定都有一个被引类型(referenced type),这个类型是对无括号的结构化绑定使用decltype
所返回的类型。举个例子:
1int arr[2] = { 1, 2 };
2auto [x, y] = arr;
那么decltype(x)
和decltype(y)
就叫被引类型。
结构化绑定有三种方式(即绑定数组、绑定类元组和绑定结构体),每种方式返回的被引类型是略有差异的。
接下来我们详细看一下这3种绑定方式的结构化绑定类型以及被引类型。
绑定数组
E为元素类型T
的数组类型,则每个元素的结构化绑定的类型是指向e数组中元素的左值;被引类型为数组元素的类型,即T
。**
若数组含有cv
修饰符,则数组元素也具有相同的cv
属性。注意结构化绑定是左值,不是左值引用。
标识符的数量必须等于数组的元素数量。
1int arr[2] = { 1, 2 };
2
3auto [x, y] = arr; //首先创建e[2],将arr拷贝到e,然后x指代e[0],y指代e[1]
4auto& [rx, ry] = arr; //首先创建e[2],是arr[2]的引用,rx指代arr[0],ry指代arr[1]
5
6//结构化绑定是左值,不是左值引用
7static_assert(std::is_reference_v<decltype(rx)>); //error: static assertion failed
绑定类元组
若E是左值引用(即ref-qualifier
包含&
,或包含&&
且initializer
是左值),则e是左值lvalue
;若E是右值引用,则e是将亡值xvalue
(实际上是完美转发)。
对于类元组的第i
个元素,令Ti = std::tuple_element<i, E>::type
,则每个结构化绑定的类型是Ti
的引用;第i
个元素的被引类型为Ti
。
注意在结构化绑定类元组的情况下,结构化绑定类型和被引类型是不同的。
对于类元组的第i
个元素的获取,若在E的作用域中对标识符get
按类成员访问进行的查找的过程中,至少找到一个声明是首个模板形参为非类型形参的函数模板,则为e.get<i>()
,否则为std::get<i>(e)
。
标识符的数量必须等于std::tuple_size<E>::value
。
1float x{ };
2char y{ };
3int z{ };
4
5std::tuple<float&, char&&, int> tpl(x, std::move(y), z);
6const auto& [a, b, c] = tpl;
7//a指名指代x的结构化绑定;decltype(a)为float&
8//b指名指代y的结构化绑定;decltype(b)为char&&
9//c指名指代z的结构化绑定;decltype(c)为const int,注意有const
10a = 1.0; //ok
11c = 2; //error: assignment of read-only reference ‘c’
12
13auto [e, f, g] = tpl; //decltype(e)为float&, decltype(f)为char&&,decltype(g)为int
上面这个例子中,const auto&
其实是修饰隐藏的变量e
的,而不是单独修饰于a
或b
或c
的。a
、b
和c
的类型需要通过std::get<i>(e)
或e.get<i>()
来获取。
绑定结构体
若结构体的数据成员mi
的类型为Ti
,则每个结构化绑定的类型是指向cv Ti
的左值;被引类型为cv Ti
。
1struct S {
2 mutable int x1 : 2;
3 volatile double y1;
4};
5
6S f()
7{
8 return {1, 2.3};
9}
10
11const auto [x, y] = f(); //x是位域的int左值,y是const volatile double左值
综上,我们可以看出结构化绑定的含义:标识符总是绑定一个对象,该对象是另一个对象的成员或数组元素,后者可以是拷贝,也可以是引用。与引用类似的是,结构化绑定都是现有对象的别名(这个对象可能是隐式的);但与引用不同,结构化绑定不一定是引用类型。
注意事项
-
声明表达式
attr(optional) cv-auto ref-qualifier(optional) [ identifier-list ] initializer
中[]
之前的修饰语全部作用于隐藏变量e
,而不是[]
中的每个新变量。 -
只要
std::tuple_size<E>
是完整类型,不管有没有包含value
成员,都一律使用类元组方式进行绑定,即使这样会造成程序非良构:
1struct A { int x; };
2
3namespace std {
4 template<> struct tuple_size<::A> { };
5}
6
7//...
8
9auto [x] = A{ }; //error: ‘std::tuple_size<A>::value’ is not an integral constant expression
上面的例子定义了std::tuple_size<::A>
,是一个完整类型,所以会采用类元组的绑定方式,而不会采用结构体的绑定方式。
- 若
expression
为纯右值,则结构化绑定的修饰符只能用const auto&
或auto&&
,auto&
不可绑定右值:
1int a = 1;
2const auto& [x] = std::make_tuple(a); //ok
3auto& [y] = std::make_tuple(a); //error
4auto&& [z] = std::make_tuple(a); //ok
- 结构化绑定不可以使用
constexpr
或static
来修饰:
1constexpr auto [x, y] = std::pair<int, int>(1, 2); //error
2static auto [z, w] = std::pair<int, int>(1, 2); //error
- 结构化绑定继承结构体是有限制的,所有非静态数据成员必须位于同一个类中,不可分散到不同的类中:
1struct A {
2 int x;
3 int y;
4};
5
6auto [x, y] = A{}; //ok
7
8struct B : A {
9 int z;
10};
11
12auto [a, b, c] = B{}; //error