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

来自cppreference.com
< cpp‎ | language

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

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

属性(可选) cv-auto 引用运算符(可选) [ 标识符列表 ] = 表达式 ; (1)
属性(可选) cv-auto 引用运算符(可选) [ 标识符列表 ]{ 表达式 }; (2)
属性(可选) cv-auto 引用运算符(可选) [ 标识符列表 ]( 表达式 ); (3)
属性 - 任意数量的属性的序列
cv-auto - 可有 cv 限定的 auto 类型说明符,也可以包含存储类说明符 staticthread_local;在 cv 限定符中包含 volatile 是被弃用的 (C++20 起)
引用运算符 - &&& 之一
标识符列表 - 此声明所引入的各标识符的逗号分隔的列表
表达式 - 顶层没有逗号运算符的表达式(文法上为赋值表达式),且具有数组或非联合类之一的类型。如果 表达式 涉及任何来自 标识符列表 的名字,那么声明非良构。

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

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

  • 如果 表达式 具有数组类型 A 且不存在 引用运算符,那么 e 具有类型 cv A,其中 cvcv-auto 序列中的 cv 限定符,且 e 中的各个元素从 表达式 的对应元素进行复制(对于 (1))或直接(对于 (2,3))初始化。
  • 否则 e 如同于声明中以其名取代 [ 标识符列表 ] 一般定义。

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

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

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

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

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

情况 1:绑定数组

标识符列表 中的每个标识符均成为指代数组的对应元素的左值。标识符的数量必须等于数组的元素数量。

每个标识符的被引用类型都是数组的元素类型。数组类型 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 必须是良构的整数常量表达式,且标识符的数量必须等于 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;
// a 指名指代 x 的结构化绑定;decltype(a) 是 float&
// b 指名指代 y 的结构化绑定;decltype(b) 是 char&&
// c 指名指代 tpl 的第 3 元素的结构化绑定;decltype(c) 是 const int

情况 3:绑定到数据成员

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

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

第 i 个标识符的被引用类型e.m_i ,如果它不是引用类型;否则是 m_i 的声明类型。

struct S
{
    mutable int x1 : 2;
    volatile double y1;
};
 
S f();
 
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
}

注解

结构化绑定不能受约束

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> {};
}
 
auto [x] = A{}; // 错误;不考虑“数据成员”解释方案。

如果存在 引用运算符表达式 为纯右值,那么应用将引用绑定到临时量的通常规则(包括生存期延续)。这些情况下,隐藏变量 e 是绑定到从纯右值表达式实质化的临时变量,并延长其生存期的一个引用。与之前一样,如果 e 是非 const 左值引用,那么绑定失败:

int a = 1;
const auto& [x] = std::make_tuple(a); // OK,非悬垂引用
auto&       [y] = std::make_tuple(a); // 错误,不能绑定 auto& 到右值 std::tuple
auto&&      [z] = std::make_tuple(a); // 同样 OK

decltype(x) 指名结构化绑定的被引用类型,其中 x 代表一个结构化绑定。在元组式的情况下,它是 std::tuple_element 所返回的类型,它可以不是引用,即使在此情况下始终会引入隐藏的引用。这相当于模拟了绑定到其各个非静态数据成员具有 tuple_element 所返回的类型的结构体的行为,而绑定自身的引用性质则只是实现细节。

std::tuple<int, int&> f();
 
auto [x, y] = f();       // decltype(x) 是 int
                         // decltype(y) 是 int&
 
const auto [z, w] = f(); // decltype(z) 是 const int
                         // decltype(w) 是 int&

lambda 表达式不能捕获结构化绑定:

#include <cassert>
 
int main()
{
    struct S { int p{6}, q{7}; };
    const auto& [b, d] = S{};
    auto l = [b, d] { return b * d; }; // C++20 起合法
    assert(l() == 42);
}
(C++20 前)

示例

#include <set>
#include <string>
#include <iomanip>
#include <iostream>
 
int main()
{
    std::set<std::string> myset{"hello"};
 
    for (int i{2}; i; --i)
    {
        if (auto [iter, success] = myset.insert("Hello"); success) 
            std::cout << "插入成功。值为 " << std::quoted(*iter) << "。\n";
        else
            std::cout << "集合中已存在值 " << std::quoted(*iter) << "。\n";
    }
 
    struct BitFields
    {
        int b : 4 {1}, d : 4 {2}, p : 4 {3}, q : 4 {4};
        // C++20 :位域的默认成员初始化器
    };
 
    {
        const auto [b, d, p, q] = BitFields{};
        std::cout << b << ' ' << d << ' ' << p << ' ' << q << '\n';
    }
 
    {
        const auto [b, d, p, q] = []{ return BitFields{4, 3, 2, 1}; }();
        std::cout << b << ' ' << d << ' ' << p << ' ' << q << '\n';
    }
    
    {
        BitFields s;
 
        auto& [b, d, p, q] = s;
        std::cout << b << ' ' << d << ' ' << p << ' ' << q << '\n';
 
        b = 4, d = 3, p = 2, q = 1;
        std::cout << s.b << ' ' << s.d << ' ' << s.p << ' ' << s.q << '\n';
    }
}

输出:

插入成功。值为 "Hello"。
集合中已存在值 "Hello"。
1 2 3 4
4 3 2 1
1 2 3 4
4 3 2 1

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 2285 C++17 表达式 可以涉及来自 标识符列表 的名字 此时声明非良构
CWG 2312 C++17 mutable 的含义在绑定到成员情况中丢失 保持其含义
CWG 2386 C++17 总是在 tuple_size<E> 为完整类型时使用“元组式”绑定协议 只会在 tuple_size<E> 拥有成员 value 时才使用
CWG 2635 C++20 结构化绑定可以受约束 已禁止
P0961R1 C++17 元组式情况中,查找找到任何类型的 get 时使用成员 get 只有在查找找到拥有非类型模板形参的函数模板才使用
P0969R0 C++17 绑定到成员情况中,要求这些成员公开 只需要在声明的语境中可访问

参阅

(C++11)
创建左值引用的 tuple,或将 tuple 解包为独立对象
(函数模板)