C++17 新特性:结构化绑定

结构化绑定定义及用法

所谓"结构化绑定", 即将指定的名称绑定到初始化器的子对象或元素上。比如有如下结构体:

1
2
3
4
struct Student {
int age;
std::string name;
};

那么有如下写法,直接把该结构体的成员绑定到新的变量名上:

1
2
Student st{18, "Tom"};
auto [a, n] = st; //auto a=n.age, auto n=s.name

结构化绑定支持的方式:

1
2
3
auto [ident-list] = expression;
auto [ident-list] {expression};
auto [ident-list](expression);

auto 前后可以使用 const alignas& 修饰。

结构化绑定可以用在 数组(array)、类元组(tuple-like)和成员变量上(data members)。

1
2
3
4
5
6
7
8
9
10
11
12
13
int tm[3] = {1949, 10, 1};
auto [y, m, d] = tm;
std::cout << m << "/" << d << "/" << y << std::endl;

std::map<int, std::string> mp = {{1, "Name"}, {2, "Age"}};
for (const auto& [k, v] : mp) {
std::cout << k << ": " << v << std::endl;
}

auto [it, rst] = mp.insert({1, "Type"});
if (!rst) {
std::cout << "Insert Error" << std::endl;
}

这么做的好处是使得代码结构更清晰,简洁易读。

细节及注意事项

1. 结构化绑定的类型

我们可以认为在结构化绑定的过程中, 其实有一个隐藏的匿名对象,就像是使用 expression 来初始化这个匿名对象的成员一样。

1
2
3
auto e = st;
auto a = e.age;
auto n = e.name;

需要特别注意的是: 结构化绑定时,修饰符是作用在匿名对象 (e) 上,而不是作用在绑定标识 (a or n) 上的
在如下代码中:

1
const auto& [a, n] = st

an 的类型为 int, 只有匿名对象 e 的类型是 const auto&
因此结构化绑定不会发生类型退化(decay)。在如下代码中,a 的类型是 int[3], a2 的类型退化为 int* .

1
2
3
4
5
6
7
struct ST{
int arr[3];
};

ST st;
auto [a] = st;
auto a2 = a;

2. 关于继承

对于继承类或结构体,结构化绑定则受到的限制:所有的成员必须在同一个类中定义。即在编译期结构化绑定必须能够完全访问类中的所有成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Animal{
std::string name;
int age;
};

struct Dog: Animal {
int legs;
};

struct Cat: Animal {
//no member
};

Dog dog;
auto [dn, da, dl] = dog; //ERROR
auto [dl2] = dog; //ERROR

Cat cat;
auto [cn, ca] = cat; //OK

3. 其它

联合体不支持使用结构化绑定。

为类支持结构化绑定

要在一个类支持结构化绑定,需要在类中实现以下代码:

1
2
3
4
#include <utility>
std::tuple_size<type>::value //绑定标识的数量
std::tuple_element<idx, type>::type //标识 idx 的类型
auto get<idx>(ClassName){ return value; } //为 idx 返回值

下面是一个实现:
对于一个类 Person:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
public:
enum class Sex {
Male,
Female,
};

Person(int age, std::string name) : age_(age), name_(name) {}

Sex sex() const { return sex_; }
void set_sex(Sex sex) { sex_ = sex; }

int age() const { return age_; }
const std::string& name() const { return name_; }

private:
int age_;
std::string name_;
Sex sex_ = Sex::Male;
};

当我们想要为此类实现结构化绑定时,有以下方法需要实现

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
template <>
struct std::tuple_size<Person> {
static constexpr int value = 3; //3个成员可以被绑定
};

// 偏特化每个绑定成员的类型
template <>
struct std::tuple_element<0, Person> {
using type = int;
};

template <>
struct std::tuple_element<1, Person> {
using type = std::string;
};

template <>
struct std::tuple_element<2, Person> {
using type = Person::Sex;
};

// 声明 get 方法
template <std::size_t>
auto get(const Person& p);

// 为每个 idx 实现 get 偏特化方法
template <>
auto get<0>(const Person& p) {
return p.age();
}

template <>
auto get<1>(const Person& p) {
return p.name();
}

template <>
auto get<2>(const Person& p) {
return p.sex();
}

之后可以使用结构化绑定:

1
2
Person p(18, "Tom");
auto [age, name, sex] = p;

注意这里我们只实现了拷贝在结构化绑定。对于引用的结构化绑定,我们还需要再实现一套。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Sex& sex() { return sex_; }
int& age() { return age_; }
std::string& name() { return name_; }

template <std::size_t>
decltype(auto) get(Person& p);

template <>
decltype(auto) get<0>(Person& p) {
return p.age();
}

template <>
decltype(auto) get<1>(Person& p) {
return p.name();
}

template <>
decltype(auto) get<2>(Person& p) {
return p.sex();
}