结构化绑定是C++17引入的一个重要的语法糖,可以让代码写得简洁不少,值得好好整理一下语法。

概述

结构化绑定(structured binding)是 C++17 标准引入的一项特性, 允许开发者在解包元组 (std::tuple)、std::pair、数组或自定义结构体等数据结构时, 将其中各个元素直接绑定到多个变量上,使得代码更加简洁易读。

基本用法

最基本的语法形如

1
auto [a0,a1,a2] = data;

这一行语句可以对data进行结构化绑定,要求:

  • 左侧必须使用auto开头(其实还可以加上const&等修饰,这里暂不讨论,见下文)
  • 左侧具体需要的变量个数由右侧数据决定。
  • a0等是合法的C++标识符,并且不能是已经定义过的标识符

结构化绑定过程中会自动用a0等作为标识符定义变量,将其依次对应data中的元素值。 假设data是一个具有三个元素的自定义结构体

1
2
3
4
5
struct Person {
std::string name;
int age;
double height;
};

那么结构化绑定的语法基本等效于

1
2
3
4
5
6
// auto [a0,a1,a2] = data;

auto tmp_data = data;
auto &a0 = tmp_data.name;
auto &a1 = tmp_data.age;
auto &a2 = tmp_data.height;

这也表明结构化绑定中发生了一次拷贝,对元素a0的修改不会影响到原本的data,反之也一样。

下面是对几类数据结构进行结构化绑定的基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <format>
#include <iostream>
#include <string>
#include <tuple>
#include <utility>

int main() {
// 解包 std::tuple
std::tuple<int, double, std::string> data{42, 3.14, "Hello"};
auto [intValue, doubleValue, strValue] = data;
std::cout << std::format(
"std::tuple - intValue: {}, doubleValue: {}, strValue: {}\n", intValue,
doubleValue, strValue);

// 解包 std::pair
std::pair<int, std::string> pairData{100, "C++"};
auto [pairInt, pairStr] = pairData;
std::cout << std::format("std::pair - pairInt: {}, pairStr: {}\n", pairInt,
pairStr);

// 解包 数组
int array[] = {10, 20, 30};
auto [arrVal1, arrVal2, arrVal3] = array;
std::cout << std::format("Array - arrVal1: {}, arrVal2: {}, arrVal3: {}\n",
arrVal1, arrVal2, arrVal3);

struct Person {
std::string name;
int age;
double height;
};

// 解包 自定义结构体
Person person = {"Alice", 30, 1.68};
auto [name, age, height] = person;
std::cout << std::format("Struct - name: {}, age: {}, height: {:.2f}\n",
name, age, height);

return 0;
}

进阶用法

前面auto开头的结构化绑定中,隐含了一次对象的复制, 我们还可以加上const&修饰得到以下几类用法:(这里以自定义结构体为例,其它数据对象是一样的)

1
2
3
4
5
6
7
8
9
10
11
12
struct Person {
std::string name;
int age;
double height;
};

auto data = Person{"Alice", 30, 1.68};

auto [a0, a1, a2] = data;
auto &[b0, b1, b2] = data;
const auto [c0, c1, c2] = data;
const auto &[d0, d1, d2] = data;

通过cppinsights去掉语法糖,可以得到编译器的具体实现可能为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Person __data16 = Person(data);
auto &a0 = __data16.name;
int &a1 = __data16.age;
double &a2 = __data16.height;

Person &__data17 = data;
auto &b0 = __data17.name;
int &b1 = __data17.age;
double &b2 = __data17.height;

const Person __data18 = Person(data);
const auto &c0 = __data18.name;
const int &c1 = __data18.age;
const double &c2 = __data18.height;

const Person &__data19 = data;
const auto &d0 = __data19.name;
const int &d1 = __data19.age;
const double &d2 = __data19.height;

这里__data16等只是编译器生成的临时标识符,无法直接获取。 对比可以发现const&实际上是作用在绑定之前的临时变量之上:

  • auto:生成tmp_data作为data的拷贝
  • auto &:生成tmp_data作为data的引用
  • const:生成tmp_data作为data的常量拷贝
  • const auto &:生成tmp_data作为data的常量引用

注意:并不是每一种自定义类(结构体)都可以被结构化绑定,必须满足如下条件:

  • 所有的非静态数据成员在当前语境中均可访问,这并不要求全部成员都是public。
  • 所有的非静态数据成员都是它自己,或者同一个基类的直接成员。

第一个要求例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Demo {
int c1, c2;
public:
Demo() = default;

void foo(Demo c){
auto [a, b] = c; // C的成员函数内,可以访问它的私有成员
}
};

int main() {
Demo c{};

auto [a, b] = c; // 外部函数无法访问C的私有成员,编译报错

return 0;
}

第二个要求例如

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A {
int a1, a2;
};
struct B : A {};
struct C : A { int c; };

int main() {
auto [x1,x2] = B{};

auto [y1,y2,y3] = C{}; // 编译报错

return 0;
}

此时对于自定义类型B可以进行结构化绑定,但是自定义类型C不行。