C++17常用新特性(三)---结构化绑定

744次阅读  |  发布于2年以前

1 结构化绑定概述

结构化绑定允许用一个对象的元素或成员同时实例化多个实体。文字说明可能显得苍白无力。下面用代码的方式来说明:

typedef struct Data {
    int i = 0;
    std::string s="Hello World";
} DATA;

int main()
{
    DATA stTmp;
    auto [u,v]=stTmp;
    std::cout<<u<<","<<v<<std::endl;
    return 0;
}

看上代码第9行是不是很熟悉,像极了Python。C++17已经正式支持了这种语法并且命名为结构化绑定,

除了上面的写法外,还支持以下几种写法:

DATA data1;
auto [u1, v1] {data1};
DATA getStructData() {
  return DATA{1, "hello C++"};
}
auto [u2,v2]=getStructData();

结构体绑定返回值可以进行独立操作,如:

if(u2 == 1){
  std::cout<<v2<<std::endl;
}

在C++17之前,如果要处理一个结构体返回对象需要先定义一个结构体变量,然后在对结构体的值进行分别处理。有了结构化绑定之后,在实际的编程时就可以直接访问返回的结构体。从某种程度上来说,增强了代码的可读性。当然这一特性除了应用在上面的场景外,也可以用于map容器的遍历。如下面的写法:

std::map<int,std::string> m_mapDataValue;
//此处省略map赋值操作
for (const auto& [key, val] : m_mapDataValue) {
  std::cout << key << ": " << val << '\n';
}

怎么样?是不是更容易理解代码,通过这种直接的方式处理,不得不说是一种进步。

2 细品结构化绑定

上面的结构化绑定技术在使用时方便了我们的操作,但是本着对待问题穷追不舍的精神,我们需要了解结构化绑定的内部处理。诚然天下没有免费的午餐,结构化绑定也是,在结构化绑定的实现过程中,里面暗藏了一个隐藏的匿名对象。而结构化绑定时指定的变量实际上就是通过这个隐藏的匿名对象进行赋值的。且看下面的代码拆解:

auto [u1, v1] {data1};

如上面的代码,在实际的运行中实际上等同于下面的这段代码:

auto e = data1;
u1 = e.i;
v1= e.s;

上面的代码说明,u1, v1是e对象的一份本地拷贝,是e.i和e.s的别名。值得注意的是,他们并不是引用的关系,如果在结构化绑定之后重新对data1进行赋值,u1和v1的值是不是随之改变的。如下面代码所示:

typedef struct Data {
    int i = 0;
    std::string s="Hello World";
} DATA;

int main()
{
    DATA stTmp;
    auto [u,v]=stTmp;
    std::cout<<u<<","<<v<<std::endl;
    stTmp.i=3;
    stTmp.s="哈,来模仿我呀";
    std::cout<<"修改数据之后[u,v]和stTmp值的变化:"<<std::endl;
    std::cout<<u<<","<<v<<std::endl;
    std::cout<<stTmp.i<<","<<stTmp.s<<std::endl;
    return 0;
}

上面代码的运行结果如下:

0,Hello World
修改数据之后[u,v]和stTmp值的变化:
0,Hello World
3,哈,来模仿我呀

运行结果也证明了上述的结论:结构化绑定只是对结构体对象的值进行拷贝,而不是引用关系。

3 哪些场景可以使用结构体绑定

原则上讲,结构化绑定适用于所有只有 public 数据成员的结构体、 C 风格数组和类似元组 (tuple­like)的对象。结构化绑定中声明的变量也必须和结构体成员的数量一致。当然,在声明结构体变量时可以使用“_”,但是在同一段代码中不能使用重复的变量,如下面的代码中编译时是会报错的。

auto [_,v]=stTmp;//可以正常编译
auto [_,v1]=stTmp;//编译时会报错

下面,将对具体的使用场景进行详细说明。

3.1 结构体和类

在前面的介绍中,使用结构化绑定时都是一些正常的场景,但是在实际编程时,结构体会使用继承。因此,在这种情况下需要遵循特定的使用原则:所有的成员变量需要在子类或者父类中统一声明。不能出现父类和子类都有定义的情况。如下代码所示:

struct Base {
    int i = 0;
    std::string s="Hello World";
};

struct Data1:public Base{

};

struct Data2:public Base{
    int c=99;
};

int main()
{
    auto [u,v]{Data1()};//可以编译通过
    auto [u1,v1,k]{Data2()}//编译报错
    return 0;
}

如上,代码编译报错的信息为:

3.2 原生数组

对原生数组使用结构化绑定时需要注意的是只有在数组的长度一定的情况下才能使用结构化绑定,且声明的对象个数要和数组长度保持一致。数组作为按值传入的参数时是不能使用结构化绑定的,这个时候数组会退化为相应的指针。如下面的代码:

int arr[] = { 47, 11 };
auto [x, y] = arr; // 正常通过
auto [z] = arr; // 编译报错,声明对象和数组长度不一致

还有一点需要注意的是,C++可以通过引用返回具有大小信息的数组,且是可以使用结构化绑定的,如:

auto getArray()->int(&)[2];
auto [u,v] = getArray();

3.3 std::pair, std::tuple 和 std::array 结构化绑定是可拓展的,可以为任何类型添加结构化绑定的支持。在C++标准库中添加了 std::pair<>、std::tuple<>、 std::array<> 的结构化绑定支持。

可以使用getArray()返回数组元素,如:

std::array<int, 4> getArray();
auto [a, b, c, d] = getArray();

在使用非临时变量的 non-const 引用进行绑定时,还能够对返回的数组元素进行修改:

std::array<int, 4> stdArr { 1, 2, 3, 4 };
auto& [a, b, c, d] = stdArr ;
a+=10;//编译正常
const auto & [e, f, g, h] = stdArr;
e+=10;//编译报错。e为常量引用
auto&& [i, j, k, l] = stdarr;
i += 10;//编译正常

同数组一样,可以通过声明相同数量的对象返回tuple中的元素。如下代码所示:

int main()
{
    std::tuple<int,std::string,float> myTuple(1,"Hello World",2.0);
    auto [a,b,c] = myTuple;
    std::cout<<a<<","<<b<<","<<c<<std::endl;
    return 0;
}

代码运行结果如下图所示:

std::pair在map值插入方法中用的比较多,通过结构化绑定可以直接对值进行操作,提高了代码的可读性。在if语句的初始化语句中有过介绍。大家可以跳转到这里:[C++17常用新特性(一)---带初始化的 if 和 switch 语句]

4 总结

在C++标准库中只对一些类型提供了结构化定义的支持,在使用时也可以自己编写自定义类型的结构化接口。具体使用根据实际编程时具体需要而定。不得不说的是结构化绑定确实帮助我们可以直接操作结构体定义的变量,提升了代码的可读性,但是从某方面说这一操作又有很多的局限性,如结构化绑定时声明的对象必须和绑定对象数量一致、对类的绑定时要求非静态成员变量必须统一在父类或者子类中进行定义等,这些要求在实际编程时带来的限制相比较而言并不是非得结构化绑定不可。因此大家在使用时需要根据实际情况,是不是为了代码可读性而牺牲掉原有的一些东西。欢迎大家留言讨论。谢谢!

- EOF -

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8