C++17结构化绑定

Share on:
600 Words | Read in about 3 Min | View times

Overview

C++17提供了结构化绑定机制,可以使用指定名称绑定到初始化的子对象或元素上,与引用绑定别名类似,但结构化绑定的类型不需要是引用类型。得益于自动推导技术越来越成熟,通过auto声明的多个变量绑定到一个复杂结构成了可能。本节内容我们来介绍C++17的新特性——结构化绑定。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

引子

回忆一下std::mapinsert函数的声明原型:

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>,那么使用这个返回值是就需要大量的firstsecond,繁琐且可读性比较差:

 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::pairfirst元素被绑定在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:可以是被constvolatile修饰的auto类型,也可以包含staticthread_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,则Ecv-A,其中cvcv-auto中的cv限定符,且e中的各个元素从expression的对应元素进行拷贝初始化或直接初始化;

  • 否则相当于定义eattr cv-auto ref-qualifier e initializere即代表着[ 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>是提供编译时判断,如果Estd::tuplestd::arraystd::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的,而不是单独修饰于abc的。abc的类型需要通过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
  • 结构化绑定不可以使用constexprstatic来修饰:
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
Prev Post: 『C++17类模板参数推导』
Next Post: 『C++17 if/switch语句初始化』