C++-结构化绑定及应用

参考自MeouSker77/Cpp17

结构化绑定

结构化绑定允许你用一个对象的元素或成员同时实例化多个实体。

概念

1
2
3
4
5
6
7
struct Test
{
int i;
std::string str;
}
Test test;
auto [intVal, strVal] = test;

这里变量intValstrVal的声明方式成为结构化绑定。某种程度上可以说它们分解了Decompose用来初始化的对象(有些观点称它们为分解声明Decomposing Declaration)。

支持的使用方式

1
2
3
4
5
6
7
8
auto [intVal, strVal] = test;
auto [intVal2, strVal2] {test};
auto [intVal3, strVal3] (test);
Test GetStruct()
{
return Test{1, "str"};
}
auto [intVal4, strVal4] = GetStruct();

应用

在基于范围的for循环中的使用

std::map中的应用

通常,在C++11遍历std::map时,使用如下方式:

1
2
3
4
5
6
std::map<int, std::string> dataMap;
// 添加数据
for(const auto& e : dataMap)
{
std::cout << e.first << " " << e.second << std::endl;
}

此处的e的类型为std::pair<int, std::string>。该类型的两个成员分别是firstsecond

但如果使用结构化绑定,可以写成如下形式:

1
2
3
4
for(const auto& [key, value] : dataMap)
{
std::cout << key << " " << value << std::endl;
}
std::tuple与struct的应用

当然,std::tuple也可以不再使用std::getindextype来获取:

1
2
3
4
5
6
7
8
9
10
11
12
std::list<std::tuple<int, char, std::string>> dataList;
// 添加数据
// 使用 std::get 与 index
for(const auto& e: dataList)
{
std::cout << std::get<0>(e) << " " << std::get<1>(e) << " " << std::get<2>(e) << std::endl;
}
// 使用结构化绑定
for (const auto &[intVal, charVal, strVal] : dataList)
{
std::cout << intVal << " " << charVal << " " << strVal << std::endl;
}

struct亦是如此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Test
{
int i;
char c;
std::string str;
};
std::list<Test> dataList;
// 添加数据
// 使用成员名访问
for(const auto& e: dataList)
{
std::cout << e.i << " " << e.c << " " << e.str << std::endl;
}
// 使用结构化绑定
for (const auto &[intVal, charVal, strVal] : dataList)
{
std::cout << intVal << " " << charVal << " " << strVal << std::endl;
}

能够大大提高代码可读性。

在带初始化的if和switch语句中的使用

有关带初始化的if和switch语句可点击链接。此处只讲解结构化绑定在此处的应用。

带初始化的if

此处的例子为向std::map或者std::unorderedmap插入元素。可以像下面这样检查是否成功:

1
2
3
4
5
6
7
std::map<std::string, int> dataMap;
...
if (auto [pos, ok] = dataMap.insert({"new", 42}); !ok) {
// 如果插入失败,用pos处理错误
const auto& [key, val] = *pos;
std::cout << "already there: " << key << '\n';
}

这里,我们用了结构化绑定给返回值的成员和pos指向的值的成员声明了新的名称, 而不是直接使用firstsecond成员。

带初始化的switch语句

通过使用带初始化的switch语句,我们可以在对条件表达式求值之前初始化一个对象/实体。

例如,我们可以先声明一个文件系统路径,然后再根据它的类别进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace fs = std::filesystem;
...
switch (fs::path p{name}; status(p).type())
{
case fs::file_type::not_found:
std::cout << p << " not found\n";
break;
case fs::file_type::directory:
std::cout << p << ":\n";
for (const auto& e : std::filesystem::directory_iterator{p})
{
std::cout << "-" << e.path() << '\n';
}
break;
default:
std::cout << p << " exists\n";
break;
}

详解结构化绑定

原理

为了理解结构化绑定,必须意识到这里面其实有一个隐藏的匿名对象

结构化绑定时引入的新变量名其实都指向这个匿名对象的成员/元素。

下面这个代码:

1
auto [intVal, strVal] = test;

其实等价于:

1
2
3
auto e = test;
aliasname intVal = e.i;
aliasname strVal = e.str;

这意味着intValstrVal仅仅是test的一份本地拷贝的成员的别名。 然而,我们没有为e声明一个名称,因此我们不能直接访问这个匿名对象。

1
std::cout << u << " " << v << std::endl;

会打印出e.i以及e.str的值(分别是test.itest.str的拷贝)。

e的生命周期和结构化绑定的生命周期相同,当结构化绑定离开作用域时e也会被自动销毁。另外,除非使用了引用,否则修改用于初始化的变量并不会影响结构化绑定引入的变量(反过来也一样)。
intValstrVal都不是引用,只有匿名实体e是一个引用。

修饰符

&

上述情况中,如果想要使结构化绑定引入的变量影响源变量,则需要使用&:

1
2
3
4
5
6
Test test{1, "std"};
auto &[intVal, strVal] = test;
test.i = 2;
std::cout << intVal << std::endl; // 2
strVal = "str";
std::cout << test.str << std::endl; // str
const

一个结构化绑定声明为const引用:

1
const auto &[intVal, strVal] = test;

这里,匿名实体被声明为const引用, 而intValstrVal分别是这个引用的成员istr的别名。 因此,对test的成员的修改会影响到u和v的值:

1
2
3
test.i = 2;
std::cout << intVal << std::endl; // 2
strVal = "str"; // error!

如果一个结构化绑定是引用类型,而且是对一个临时对象的引用,那么和往常一样, 临时对象的生命周期会被延长到结构化绑定的生命周期:

1
2
3
4
5
6
Test GetStruct()
{
return Test{1, "str"};
}
const auto &[intVal, strVal] = GetStruct();
std::cout << intVal << " " << strVal << std::endl; // no error

移动语义

如下声明:

1
2
Test test = {1, "str"};
auto&& [intVal, strVal] = std::move(test); // 匿名实体是test的右值引用

这里intValstrVal指向的匿名实体是test的右值引用, 同时test仍持有值。

然而,你可以对指向test.strstrVal进行移动赋值:

1
2
3
4
std::string s = std::move(strVal);                      // 把test.str移动到s
std::cout << "test.str: " << test.str << std::endl; // 打印出未定义的值
std::cout << "strVal: " << strVal << '\n'; // 打印出未定义的值
std::cout << "s: " << s << '\n'; // 打印出"str"

像通常一样,值被移动走的对象处于一个值未定义但却有效的状态。因此可以打印它们的值, 但不要对打印出的值做任何假设。

注意

  1. 在任何情况下,结构化绑定中声明的变量名的数量都必须和元素或数据成员的数量相同。 你不能跳过某个元素,也不能重复使用变量名。然而,你可以使用非常短的名称例如’_‘ (有的程序员喜欢这个名字,有的讨厌它,但注意全局命名空间不允许使用它),但这个名字在同一个作用域只能使用一次:
1
2
auto [_, val1] = getStruct();   // OK
auto [_, val2] = getStruct(); // error:变量名_已经被使用过
  1. 目前还不支持嵌套化的结构化绑定。