结构化绑定允许通过对象的元素或成员初始化多个实体。
例如,假设你定义了一个结构体包含两个成员:
struct MyStruct
{
int i = 0;
std::string s;
};
MyStruct ms;
可以使用以下声明将该结构的成员直接绑定到新名称:
auto [u,v] = ms;
这里,u和v的名称就是所谓的结构化绑定。
结构化绑定对于返回结构或数组的函数尤其有用。
例如,假设您有一个返回结构的函数:
MyStruct getStruct()
{
return MyStruct{42, "hello"};
}
在这里可以直接将结果分配给两个实体,并为返回的数据成员提供本地名称:
auto[id,val] = getStruct();
这里,id和val是返回结构的成员i和s的名称。它们有相应的类型int和std::string,可以作为两个不同的对象使用:
if (id > 30)
{
std::cout << val;
}
这样做的好处是直接访问和将值直接绑定到表示其目的语义的名称,从而使代码更具可读性
要在没有结构化绑定的情况下迭代std::map<>的元素,编写程序:
for (const auto& elem : mymap)
{
std::cout << elem.first << ": " << elem.second << '\n';
}
通过使用结构化绑定,代码变得可读性更强:
for (const auto& [key,val] : mymap)
{
std::cout << key << ": " << val << '\n';
}
我们可以直接使用每个元素的键和值成员,使用名称清楚地显示它们具体的含义。
注意对于结构体,结构化绑定不能绑定static成员:
struct MyStruct
{
int i = 0;
const static int a;
std::string s;
};
const int MyStruct::a = 19;
MyStruct ms{100, "hhhh"};
int main()
{
auto [id, name] = ms;//OK
//auto [id, ss, name] = ms;//error: 3 names provided for structured binding, while ‘MyStruct’ decomposes into 2 elements
std::cout << id << ", " << name << std::endl;
}
1. 接下来详细介绍下structured bindings
为了理解结构化绑定,必须注意其中涉及一个新的匿名变量。作为结构绑定引入的新名称引用这个匿名变量的成员/元素。
auto [u, v] = ms;
其行为就像我们用ms初始化一个新的实体e,让结构化绑定u和v成为这个新对象成员的别名,类似于定义:
auto e = ms;
auto& u = e.i;
auto& v = e.s;
唯一的区别是我们没有e的名称,所以我们不能直接通过名称访问初始化的实体。
std::cout << u << ' ' << v << '\n'; //e.i和e.s的值,是从ms.i和ms.s拷贝过来的。
只要到它的结构化绑定存在,匿名的这个e就存在。因此,当结构化绑定超出范围时,它将被销毁。
因此,除非使用引用,否则修改用于初始化的值不会影响结构化绑定初始化的名称(反之亦然):
MyStruct ms{42,"hello"};
auto [u,v] = ms;
ms.i = 77;
std::cout << u; // prints 42
u = 99;
std::cout << ms.i; // prints 77
u和ms.i有不同的地址。
当为返回值使用结构化绑定时,应用相同的原则。初始化,如
auto [u,v] = getStruct();
行为就像我们用getStruct()的返回值初始化一个新的实体e,以便结构化绑定u和v成为e的两个成员/元素的别名,类似于定义:
auto e = getStruct();
auto& u = e.i;
auto& v = e.s;
也就是说,结构化绑定绑定到从返回值初始化的新实体,而不是直接绑定到返回值。
对匿名变量e的地址和字节对齐也得到了保证,以便结构化绑定与它们绑定到的相应成员对齐。
例1:
#include <iostream>
#include <string>
#include <assert.h>
struct MyStruct
{
int i = 0;
std::string s;
};
MyStruct ms{42, "hello"};
int main()
{
auto [u,v] = ms;
assert(&((MyStruct*)&u)->s == &v); //
return 0;
}
这里,((MyStruct*)&u)生成一个指向匿名实体的指针。u和v的地址和字节对齐和结构体里面变量一样的规则。
具体可以看下调试结果:
我们可以使用限定符,如const和reference。同样,这些限定符也适用于整个匿名实体e。通常,效果类似于直接将限定符应用于结构化绑定,但是要注意,情况并非总是如此(请看下面的内容)。
例如,我们可以声明到const引用的结构化绑定:
const auto& [u,v] = ms; // a reference, so that u/v refer to ms.i/ms.s
这里,匿名实体被声明为一个const引用,这意味着u和v是初始化的对ms的const引用的成员i和s的名称,因此,对ms成员的任何更改都会影响u和/或v的值。
ms.i = 77; // affects the value of u
std::cout << u; // prints 77
声明为非const引用,您甚至可以修改用于初始化的对象/值的成员:
MyStruct ms{42,"hello"};
auto& [u,v] = ms; // the initialized entity is a reference to ms
ms.i = 77; // affects the value of u
std::cout << u; // prints 77
u = 99; // modifies ms.i
std::cout << ms.i; // prints 99
如果用于初始化结构化绑定引用的值是一个临时对象,则通常将临时对象的生存期扩展到结构化绑定的生存期:
MyStruct getStruct();
...
const auto& [a,b] = getStruct(); //在这里const auto&既可以用到左值也可以用到右值上。
std::cout << "a: " << a << '\n'; // OK
2.限定符不一定适用于结构化绑定:
如前所述,限定符适用于新的匿名实体,而不一定适用于作为结构化绑定引入的新名称。这一点的区别可以通过指定对齐方式来说明:
alignas(16) auto [u,v] = ms; // align the object, not v /* 理解不是很好,gdb没有调试出什么结果*/
在这里,我们对齐初始化的匿名实体,而不是结构化绑定u和v。
出于同样的原因,虽然使用了auto,但结构化绑定不会衰变。如果我们有一个原始数组结构:
例2:
#include <iostream>
struct S
{
const char x[6];
const char y[3];
};
int main()
{
S s1{};
auto [a, b] = s1; // a and b get the exact member types
return 0;
}
结果如下:
a的类型仍然是const char[6]。同样,自动应用于匿名实体,它作为一个整体不会衰减。这不同于初始化一个新的对象与自动,其中类型衰减:
例3:
#include <iostream>
struct S
{
const char x[6];
const char y[3];
};
int main()
{
S s1{};
auto [a, b] = s1;
auto a2 = a; // a2 gets decayed type of a
return 0;
}
结果如下:
3.移动语义
移动语义的规则如下:
MyStruct ms = { 42, "Jim" };
auto&& [v,n] = std::move(ms); // 这里新的匿名实体e相当于是对ms的右值引用,并没有真正移动ms,[知识点1]
结构化绑定v和n表示一个匿名实体e作为ms的右值引用,ms仍然保持其值:
std::cout << "ms.s: " << ms.s << '\n'; // prints "Jim" ,这里可以打印ms.s
但是你可以移动赋值n,也就是ms.s:
std::string s = std::move(n); // moves ms.s to s
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints unspecified value
std::cout << "s: " << s << '\n'; // prints "Jim"
与移动规则一样,从对象中移出的对象处于有效状态,值未指定。因此,可以打印值,但不要对打印的内容做任何假设
这与用ms的移动值初始化新实体[知识点1]略有不同:
MyStruct ms = { 42, "Jim" };
auto [v,n] = std::move(ms); // 这里新的匿名实体e从ms移动了值
这里,初始化的匿名实体是一个新对象,初始化时使用的是值是从ms移动来的。因此, ms已经丢失了它的值:
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints "Jim"
仍然可以移动赋值n或在那里赋值,但这不影响ms.s
std::string s = std::move(n); // moves n to s
n = "Lara";
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints "Lara"
std::cout << "s: " << s << '\n'; // prints "Jim"
4.哪些地方可以使用结构化绑定使用
原则上,结构化绑定可用于具有public数据成员、原始C语言风格的数组和像tuple元组对象的结构:
a. 如果在结构体和类中所有非静态数据成员都是公共的,则可以将每个非静态数据成员绑定到一个名称。
b. 对于原始数组,可以将名称绑定到每个元素.
c. 对于任何类型,都可以使用像tuple元组API将名称绑定到API定义为“元素”的任何内容。[后续介绍如何提供tuple一样的API接口]
该API大致由以下元素组成的类型:
- std::tuple_size<type>::value必须返回元素的数量。
- std::tuple_element<idx,type>::type必须返回idxth元素的类型。
-全局或成员get<idx>()必须生成idx元素的值。
描述的可能不太清楚,后面看例子就会明白了。
标准库类型std::pair<>, std::tuple<>, std::array<>已经提供了这个API。
如果结构或类提供了像tuple元组API,则使用该API。
在所有情况下,元素或数据成员的数量都必须符合结构化绑定声明中的名称数量。你不能跳过名字,也不能重复使用名字。但是,您可以使用一个非常短的名称,比如“_”,但是在相同的范围内只能使用一次:
auto [_,val1] = getStruct(); // OK
auto [_,val2] = getStruct(); // ERROR: name _ already used
接下来将详细介绍以上的所有情况。
4.1 struct 和 class
上面的示例演示了一些用于结构和类的结构化绑定的简单案例。
注意,只有有限的继承用法是可能的。所有非静态数据成员必须是相同类定义的成员(因此,它们必须是类型的直接成员或相同的明确的公共基类的直接成员):
例4:
#include <iostream>
struct B {
int a = 1;
int b = 2;
};
struct D1 : B {
};
struct D2 : B {
int c = 3;
};
int main()
{
auto [x, y] = D1{}; // OK
//auto [i, j, k] = D2{}; // Compile-Time ERROR
std::cout << "x=" << x << ", y=" << y << std::endl;
return 0;
}
结果:
4.2 原始数组
下面的代码通过原始c风格数组的两个元素初始化x和y:
例5:
#include <iostream>
int main()
{
int arr[] = { 47, 11 };
auto [x, y] = arr;
//auto [z] = arr; // ERROR: number of elements doesn’t fit
std::cout << "x=" << x << ", y=" << y << std::endl;
return 0;
}
结果:
当然,这只有在数组仍然具有已知大小的情况下才有可能。对于作为参数传递的数组,这是不可能的,因为它衰减为对应的指针类型。
注意c++允许我们通过引用返回大小数组,所以这个特性也适用于返回数组的函数,前提是数组的大小是返回类型的一部分:
例6:
#include <iostream>
int arr[2] = {24, 42};
auto getArrayRef() -> int(&)[2]
{
return arr;
}
decltype(arr)& getArrayRef2()
{
return arr;
}
int main()
{
auto& r1 = getArrayRef();
auto r2 = getArrayRef();
auto& r3 = getArrayRef2();
auto r4 = getArrayRef2();
auto [x, y] = getArrayRef2();
return 0;
}
结果:
4.3 结构化绑定std::pair, std::tuple, and std::array
结构化绑定机制是可扩展的,因此可以向任何类型添加对结构化绑定的支持。标准库里面有std::pair<>, std::tuple<>, and std::array<>等。
4.3.1 std::array
在这里,i,j,k,l结构化绑定到了getArray的返回值std::array的元素上,而且也支持写访问,如变量w,只要绑定的对象不是一个临时的右值。
例7:
#include <iostream>
#include <array>
constexpr int size = 4;
std::array<int, size> getArray()
{
return std::array<int, size>{11,22,33,44};
}
int main(void)
{
std::array<int, size> stdarr{ 1,2,3,4 };
auto [i, j, k, l] = getArray(); //i, j, k, l name the 4 elements of the copied return value
auto& [w, x, y, z] = stdarr;
w += 10; //支持写访问
std::cout << stdarr[0] << std::endl;
return 0;
}
结果如下:
4.3.2 tuple
如下代码通过getTuple返回std::tuple<>的三个元素初始化了变量a,b,c。也就是说,a得到了类型char,b得到了类型float,c得到了std::string。
#include <iostream>
#include <tuple>
#include <string>
std::tuple<char, float, std::string> getTuple()
{
return std::make_tuple('x', 3.14, "PI");
}
int main()
{
auto [a, b, c] = getTuple();
return 0;
}
结果如下:
4.3.3 std::pair
作为另一个示例中,代码来处理调用的返回值插入()在一个关联/无序容器可以直接通过绑定的值,使更可读名称传达语义的目的,而不是依靠通用名称的frist和second从std::pair<>对象:
#include <iostream>
#include <map>
#include <string>
int main(void)
{
std::map<std::string, int> coll{ {"new", 42} };
auto [iter, result] = coll.emplace("new", 42);
if (not result)
{
//if emplace failed.
std::cout << "emplace(\"new\", 42) failed" << std::endl;
}
else
{
std::cout << "emplace(\"new\", 42) successful" << std::endl;
}
return 0;
}
结果如下:
在c++17之前我们需要如下书写方式:
auto ret = coll.insert({"new",42});
if (!ret.second){
// if insert failed, handle error using iterator ret.first
...
}
注意,在这个特殊的例子中,c++ 17提供了一种方法,可以使用if和初始化声明进一步改进这一点。