结构化绑定声明 (C++17 起)

来自cppreference.com


 
 
C++ 语言
 
 

将指定名称绑定到初始化器的子对象或元素。

与引用类似,结构化绑定是既存对象的别名。不同于引用的是,结构化绑定的类型不需要是引用类型。

属性 (可选) 声明说明符序列 引用限定符 (可选) [ 绑定标识符列表 ] 初始化器 ;
属性 - 任意数量的属性的序列
decl-specifier-seq - 包含以下说明符的序列(遵循简单声明的规则):
(C++26 起)
引用限定符 - &&& 之一
绑定标识符列表 - 此声明所引入的各标识符的逗号分隔的列表,每个标识符可以后随一个属性说明符序列(C++26 起)
初始化器 - 初始化器(见下文)


初始化器 必须是下列之一:

= 表达式 (1)
{ 表达式 } (2)
( 表达式 ) (3)
表达式 - (无括号的逗号表达式以外的)任意表达式


结构化绑定声明将绑定标识符列表 中的所有标识符引入作为其外围作用域中的名字,并将它们绑定到表达式 所指代的对象的各个子对象或元素。以此方式引入的绑定被称作结构化绑定

绑定标识符列表 中可以有一个前附省略号的标识符。这种标识符会引入结构化绑定包

该标识符必须声明模板化实体

(C++26 起)

结构化绑定是绑定标识符列表 中的一个没有前导省略号的(C++26 起)标识符,或者同一个标识符列表中引入的结构化绑定包的一个元素(C++26 起)

绑定过程

结构化绑定声明首先引入一个唯一命名的变量(此处以 e 指代)来保有其初始化器的值,方式如下:

  • 如果表达式 具有数组类型 cv1 A 且不存在引用限定符,那么通过 属性 (可选) 标识符 A e; 来定义 e,其中标识符 是包含声明说明符序列 的除 auto 以外的所有说明符的序列。
然后以初始化器 对应的方式以表达式 中的元素初始化 e 中的对应元素:
  • 否则通过 属性 (可选) 声明说明符序列 引用限定符 (可选) e 初始化器 ; 来定义 e

我们用 E 代表标识表达式 e 的类型。(换言之,E 等价于 std::remove_reference_t<decltype((e))>)。

E结构化绑定大小 是结构化绑定声明所要引入的结构化绑定的个数。

绑定标识符列表 中标识符的个数必须等于 E 的结构化绑定大小。

(C++26 前)

给定绑定标识符列表 中标识符的个数 NE 的结构化绑定大小 S

  • 如果不存在结构化绑定包,那么 N 必须等于 S
  • 否则非包元素的个数(即 N - 1)必须小于或等于 S,并且结构化绑定包的元素个数是 S - N + 1(该值可以为零)。
(C++26 起)
struct C { int x, y, z; };

template <class T>
void now_i_know_my() 
{
    auto [a, b, c] = C(); // OK: a, b, c 分别指代 x, y, z
    auto [d, ...e] = C(); // OK: d 指代 x; ...e 指代 y 和 z
    auto [...f, g] = C(); // OK: ...f 指代 x 和 y; g 指代 z
    auto [h, i, j, ...k] = C(); // OK: 包 k 为空
    auto [l, m, n, o, ...p] = C(); // 错误: 结构化绑定大小太小
}

结构化绑定可以以下三种可能的方式之一进行绑定,取决于 E

  • 情况 1:如果 E 是数组类型,那么绑定各个名字到各个数组元素。
  • 情况 2:如果 E 是非联合类类型且 std::tuple_size<E> 是完整类型并拥有名为 value 的成员(无关乎这种成员的类型或可访问性),那么使用“元组式”绑定协议。
  • 情况 3:如果 E 是非联合类类型但 std::tuple_size<E> 不是完整类型,那么绑定各个名字到 E 的各个可访问数据成员。

下文对每种情况都有更详细的描述。

每个结构化绑定都有一个被引用类型,会在后文的描述中予以定义。此类型是对无括号的结构化绑定应用 decltype 所返回的类型。

情况 1:绑定数组

绑定标识符列表 中的每个结构化绑定均成为指代数组的对应元素的左值的名字。E 的结构化绑定大小等于数组的元素数量。

每个结构化绑定的被引用类型 都是数组的元素类型。注意如果数组类型 E 有 cv 限定,那么它的元素类型也有同样的限定。

int a[2] = {1, 2};

auto [x, y] = a;    // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]

情况 2:绑定实现元组操作的类型

表达式 std::tuple_size<E>::value 必须是良构的整数常量表达式,且 E 的结构化绑定大小等于 std::tuple_size<E>::value

对于每个结构化绑定,引入一个类型为“std::tuple_element<I, E>::type 的引用”的变量:如果它对应的初始化器是左值,那么它是左值引用,否则它是右值引用。第 I 个变量的初始化器

  • 如果在 E 的作用域中对标识符 get 按类成员访问进行的查找中,至少找到一个声明是首个模板形参为非类型形参的函数模板,那么它是 e.get<I>()
  • 否则它是 get<I>(e),其中 get 只会进行实参依赖查找,忽略其他查找。

这些初始化器表达式中,如果实体 e 的类型是左值引用(只会在引用限定符 是 &,或它是 && 且初始化器是左值时发生),那么 e 是左值,否则它是亡值(这实际上进行了一种完美转发),Istd::size_t 的纯右值,而且始终将 <I> 解释为模板形参列表。

该变量拥有与 e 相同的存储期

然后该结构化绑定变成左值的名字,它指代与上述变量绑定的对象。

I 个结构化绑定的被引用类型 是 std::tuple_element<I, E>::type

float x{};
char  y{};
int   z{};

std::tuple<float&, char&&, int> tpl(x, std::move(y), z);
const auto& [a, b, c] = tpl;
// using Tpl = const std::tuple<float&, char&&, int>;
// a 指名指代 x 的结构化绑定(以 get<0>(tpl) 初始化)
// decltype(a) 是 std::tuple_element<0, Tpl>::type,即 float&
// b 指名指代 y 的结构化绑定(以 get<1>(tpl) 初始化)
// decltype(b) 是 std::tuple_element<1, Tpl>::type,即 char&&
// c 指名指代 tpl 的第 3 元素(get<2>(tpl))的结构化绑定
// decltype(c) 是 std::tuple_element<2, Tpl>::type,即 const int

情况 3:绑定到数据成员

E 的所有非静态数据成员必须都是 EE 的同一基类的直接成员,必须在指名为 e.name 时在结构化绑定的语境中是良构的。E 不能有匿名联合体成员。E 的结构化绑定大小等于非静态数据成员的数量。

绑定标识符列表 中的各个结构化绑定,按声明顺序依次成为指代 e 的各个成员的左值的名字(支持位域);该左值的类型是 e.mI 的类型,其中 mI 代表第 I 个成员。

I 个结构化绑定的被引用类型 是 e.mI(如果它不是引用类型);否则是 mI 的声明类型。

#include <iostream>

struct S
{
    mutable int x1 : 2;
    volatile double y1;
};

S f() { return S{1, 2.3}; }

int main()
{
    const auto [x, y] = f(); // x 是标识 2 位位域的 int 左值
                             // y 是 const volatile double 左值
    std::cout << x << ' ' << y << '\n';  // 1 2.3
    x = -2;   // OK
//  y = -2.;  // 错误:y 具有 const 限定
    std::cout << x << ' ' << y << '\n';  // -2 2.3
}

初始化顺序

valI绑定标识符列表 中第 I 个结构化绑定指名的对象或引用:

  • e 的初始化按顺序早于任何 valI 的初始化。
  • 如果 I 小于 J,那么任何 valI 的初始化都按顺序早于任何 valJ 的初始化。

注解

结构化绑定不能受约束

template<class T>
concept C = true;

C auto [x, y] = std::pair{1, 2}; // 错误:受约束
(C++20 起)

对成员 get 的查找照常忽略可访问性,同时也忽略非类型模板形参的确切类型。出现私有的 template<char*> void get(); 成员将导致使用成员解释方案,即使这会导致程序非良构也是如此。

声明中 [ 之前的部分应用于隐藏变量 e,而非引入的各个结构化绑定:

int a = 1, b = 2;
const auto& [x, y] = std::tie(a, b); // x 与 y 的类型是 int&
auto [z, w] = std::tie(a, b);        // z 与 w 的类型仍然是 int&
assert(&z == &a);                    // 通过

如果 std::tuple_size<E> 是完整类型,那么始终使用元组式解释方案,即使这会导致程序非良构也是如此:

struct A { int x; };

namespace std
{
    template<>
    struct tuple_size<::A> { void value();