c++17之结构化绑定

30 篇文章 17 订阅

结构化绑定允许通过对象的元素或成员初始化多个实体。

例如,假设你定义了一个结构体包含两个成员:

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和初始化声明进一步改进这一点。

  • 13
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值