首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >聊聊结构化绑定

聊聊结构化绑定

作者头像
高性能架构探索
发布于 2023-09-05 10:18:54
发布于 2023-09-05 10:18:54
45000
代码可运行
举报
文章被收录于专栏:技术随笔心得技术随笔心得
运行总次数:0
代码可运行

动机

std::map<K, V>insert方法返回std::pair<iterator, bool>,两个元素分别是指向所插入键值对的迭代器与指示是否新插入元素的布尔值,而std::map<K, V>::iterator解引用又得到键值对std::pair<const K, V>。在一个涉及std::map的算法中,有可能出现大量的firstsecond,让人不知所措。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <map>

int main()
{
    typedef std::map<int, int> Map;
    Map map;
    std::pair<Map::iterator, bool> result = map.insert(Map::value_type(1, 2));
    if (result.second)
        std::cout << "inserted successfully" << std::endl;
    for (Map::iterator iter = map.begin(); iter != map.end(); ++iter)
        std::cout << "[" << iter->first << ", " << iter->second << "]" << std::endl;
}

C++11标准库添加了std::tie,用若干引用构造出一个std::tuple,对它赋以std::tuple对象可以给其中的引用一一赋值(二元std::tuple可以由std::pair构造或赋值)。std::ignore是一个占位符,所在位置的赋值被忽略。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <map>
#include <utility>

int main()
{
    std::map<int, int> map;
    bool inserted;
    std::tie(std::ignore, inserted) = map.insert({1, 2});
    if (inserted)
        std::cout << "inserted successfully" << std::endl;
    for (auto&& kv : map)
        std::cout << "[" << kv.first << ", " << kv.second << "]" << std::endl;
}

但是这种方法仍远不完美,因为:

•变量必须事先单独声明,其类型都需显式表示,无法自动推导;•对于默认构造函数执行零初始化的类型,零初始化的过程是多余的;•也许根本没有可用的默认构造函数,如std::ofstream

为此,C++17引入了结构化绑定(structured binding)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <map>

int main()
{
    std::map<int, int> map;
    auto&& [iter, inserted] = map.insert({1, 2});
    if (inserted)
        std::cout << "inserted successfully" << std::endl;
    for (auto&& [key, value] : map)
        std::cout << "[" << key << ", " << value << "]" << std::endl;
}

结构化绑定这一语言特性在提议的阶段曾被称为分解声明(decomposition declaration),后来又被改回结构化绑定。这个名字想强调的是,结构化绑定的意义重在绑定而非声明。

语法

结构化绑定有三种语法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] = expression;
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] { expression };
attr(optional) cv-auto ref-operator(optional) [ identifier-list ] ( expression );

其中,attr(optional)为可选的attributes,cv-auto为可能有constvolatile修饰的autoref-operator(optional)为可选的&&&identifier-list为逗号分隔的标识符,expression为单个表达式。

另外再定义initializer= expression{ expression }( expression ),换言之上面三种语法有统一的形式attr(optional) cv-auto ref-operator(optional) [ identifier-list ] initializer;

整个语句是一个结构化绑定声明,标识符也称为结构化绑定(structured bindings),不过两处“binding”的词性不同。

顺带一提,C++20中volatile的许多用法都被废弃了。

行为

结构化绑定有三类行为,与上面的三种语法之间没有对应关系。

第一种情况,expression是数组,identifier-list的长度必须与数组长度相等。

第二种情况,对于expression的类型Estd::tuple_size<E>是一个完整类型,则称E为类元组(tuple-like)类型。在STL中,std::arraystd::pairstd::tuple都是这样的类型。此时,identifier-list的长度必须与std::tuple_size<E>::value相等,每个标识符的类型都通过std::tuple_element推导出(具体见后文),用成员get<I>()get<I>(e)初始化。显然,这些标准库设施是与语言核心绑定的。

第三种情况,E是非union类类型,绑定非静态数据成员。所有非静态数据成员都必须是public访问属性,全部在E中,或全部在E的一个基类中(即不能分散在多个类中)。identifier-list按照类中非静态数据成员的声明顺序绑定,数量相等。

应用

结构化绑定擅长处理纯数据类型,包括自定义类型与std::tuple等,给实例的每一个字段分配一个变量名:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>

struct Point
{
    double x, y;
};

Point midpoint(const Point& p1, const Point& p2)
{
    return { (p1.x + p2.x) / 2, (p1.y + p2.y) / 2 };
}

int main()
{
    Point p1{ 1, 2 };
    Point p2{ 3, 4 };
    auto [x, y] = midpoint(p1, p2);
    std::cout << "(" << x << ", " << y << ")" << std::endl;
}

配合其他语法糖,现代C++代码可以很优雅:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <map>

int main()
{
    std::map<int, int> map;
    if (auto&& [iter, inserted] = map.insert({ 1, 2 }); inserted)
        std::cout << "inserted successfully" << std::endl;
    for (auto&& [key, value] : map)
        std::cout << "[" << key << ", " << value << "]" << std::endl;
}

利用结构化绑定在类元组类型上的行为,我们可以改变数据类型的结构化绑定细节,包括类型转换、是否拷贝等:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <string>
#include <utility>

class Transcript { /* ... */ };

class Student
{
public:
    const char* name;
    Transcript score;
    std::string getName() const { return name; }
    const Transcript& getScore() const { return score; }
    template<std::size_t I>
    decltype(auto) get() const
    {
        if constexpr (I == 0)
            return getName();
        else if constexpr (I == 1)
            return getScore();
        else
            static_assert(I < 2);
    }
};

namespace std
{
template<>
struct tuple_size<Student>
    : std::integral_constant<std::size_t, 2> { };

template<>
struct tuple_element<0, Student> { using type = decltype(std::declval<Student>().getName()); };

template<>
struct tuple_element<1, Student> { using type = decltype(std::declval<Student>().getScore()); };
}

int main()
{
    std::cout << std::boolalpha;
    Student s{ "Jerry", {} };
    const auto& [name, score] = s;
    std::cout << name << std::endl;
    std::cout << (&score == &s.score) << std::endl;
}

Student是一个数据类型,有两个字段namescorename是一个C风格字符串,它大概是从C代码继承来的,我希望客户能用上C++风格的std::stringscore属于Transcript类型,表示学生的成绩单,这个结构比较大,我希望能传递const引用以避免不必要的拷贝。为此,我写明了三要素:std::tuple_sizestd::tuple_elementget。这种机制给了结构化绑定很强的灵活性。

细节

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <utility>
#include <tuple>

int main()
{
    std::pair pair{ 1, 2.0 };
    int number = 3;
    std::tuple<int&> tuple(number);
    const auto& [i, f] = pair;
    //i = 4; // error
    const auto& [ri] = tuple;
    ri = 5;
}

如果结构化绑定i被声明为const auto&,对应的类型为int,那么它应该是个const int&吧?i = 4;出错了,看起来正是如此。但是如何解释ri = 5;是合法的呢?

这个问题需要系统地从头谈起。先引入一个名字eE为其类型:

•当expression是数组类型A,且ref-operator不存在时,Ecv A,每个元素由expression中的对应元素拷贝(= expression)或直接初始化({ expression }( expression );•否则,相当于定义eattr cv-auto ref-operator e initializer;

也就是说,方括号前面的修饰符都是作用于e的,而不是那些新声明的变量。至于为什么第一条会独立出来,这是因为在标准C++中第二条的形式不能用于数组拷贝。

然后分三种情况讨论:

•数组情形,ET的数组类型,则每个结构化绑定都是指向e数组中元素的左值;被引类型(referenced type)为T;——结构化绑定是左值,不是左值引用:int array[2]{ 1, 2 }; auto& [i, j] = array; static_assert(!std::is_reference_v<decltype(i)>);;•类元组情形,如果e是左值引用,则e是左值(lvalue),否则是消亡值(xvalue);记Tistd::tuple_element<i, E>::type,则结构化绑定vi的类型是Ti的引用;当get返回左值引用时是左值引用,否则是右值引用;被引类型为Ti;——decltype对结构化绑定有特殊处理,产生被引类型,在类元组情形下结构化绑定的类型与被引类型是不同的;•数据成员情形,与数组类似,设数据成员mi被声明为Ti类型,则结构化绑定的类型是指向cv Ti的左值(同样不是左值引用);被引类型为cv Ti

至此,我想“结构化绑定”的意义已经明确了:标识符总是绑定一个对象,该对象是另一个对象的成员(或数组元素),后者或是拷贝或是引用(引用不是对象,意会即可)。与引用类似,结构化绑定都是既有对象的别名(这个对象可能是隐式的);与引用不同,结构化绑定不一定是引用类型。

(不理解的话可以参考N4659 11.5节,尽管你很可能会更加看不懂……)

现在可以解释riconst的现象了:编译器先创建了变量const auto& e = tuple;Econst std::tuple<int&>&std::tuple_element<0, E>::typeint&std::get<0>(e)同样返回int&,故riint&类型。

在面向底层的C++编程中常用union和位域(bit field),结构化绑定支持这样的数据成员。如果类有union类型成员,它必须是命名的,绑定的标识符的类型为该union类型的左值;如果有未命名的union成员,则这个类不能用于结构化绑定。

C++中不存在位域的指针和引用,但结构化绑定可以是指向位域的左值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>

struct BitField
{
    int f1 : 4;
    int f2 : 4;
    int f3 : 4;
};

int main()
{
    BitField b{ 1, 2, 3 };
    auto& [f1, f2, f3] = b;
    f2 = 4;
    auto print = [&] { std::cout << b.f1 << " " << b.f2 << " " << b.f3 << std::endl; };
    print();
    f2 = 21;
    print();
}

程序输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1 4 3
1 5 3

f2的功能就像位域的引用一样,既能写回原值,又不会超出位域的范围。

还有一些语法细节,比如get的名字查找、std::tuple_size<E>没有valueexplicit拷贝构造函数等,除非是深挖语法的language lawyer,在实际开发中不必纠结(上面这一堆已经可以算language lawyer了吧)。

局限

以上代码示例应该已经囊括了所有类型的结构化绑定应用,你能想象到的其他语法都是错的,包括但不限于:

•用std::initializer_list<T>初始化;因为std::initializer_list<T>的长度是动态的,但结构化绑定的标识符数量是静态的。•用列表初始化——auto [x,y,z] = {1, "xyzzy"s, 3.14159};;这相当于声明了三个变量,但结构化绑定的意图在于绑定而非声明。•不声明而直接绑定——[iter, success] = mymap.insert(value);;这相当于用std::tie,所以请继续用std::tie。另外,由[开始可能与attributes混淆,给编译器和编译器设计者带来压力。•指明结构化绑定的修饰符——auto [& x, const y, const& z] = f();;同样是脱离了结构化绑定的意图。如果需要这样的功能,或者一个个定义变量,或者手动写上三要素。•指明结构化绑定的类型——SomeClass [x, y] = f();auto [x, std::string y] = f();;第一种可用auto [x, y] = SomeClass{ f() };代替;第二种同上一条。•显式忽略一个结构化绑定——auto [x, std::ignore, z] = f();;消除编译器警告是一个理由,但是auto [x, y, z] = f(); (void)y;亦可。这还涉及一些语言问题,请移步P0144R2 3.8节。•标识符嵌套——std::tuple<T1, std::pair<T2, T3>, T4> f(); auto [ w, [x, y], z ] = f();;多写一行吧。[同样可能与attributes混淆。

以上语法都没有纳入C++20标准,不过可能在将来成为C++语法的扩展。

延伸

C++17的新特性不是孤立的,与结构化绑定相关的有:

•类模板参数推导(class template argument deduction,CTAD),由构造函数参数推导类模板参数;•拷贝省略(copy elision),保证NRV(named return value)优化;•constexpr if,简化泛型代码,消除部分SFINAE;•带初始化的条件分支语句:语法糖,使代码更加优雅。

推荐阅读 点击标题可跳转

1、性能优化利器之constexpr

2、我们通常说的POD到底是什么?

3、心心念念的优化完成了,虽然不是很完美

你好,我是雨乐,从业十二年有余,历经过传统行业网络研发、互联网推荐引擎研发,目前在广告行业从业8年。目前任职某互联网公司高级技术专家一职,负责广告引擎的架构和研发。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C++11 auto关键字:原理解析与使用指南
在C++11标准中,auto关键字经历了语义上的彻底革新。在此之前(C++98/03),auto作为存储类说明符,用于标识变量的自动存储周期(默认行为,极少显式使用),几乎处于废弃状态。C++11重新定义了auto的语义,使其成为类型占位符,允许编译器根据变量的初始化表达式自动推导类型。这一特性极大简化了复杂类型声明(如STL容器迭代器、模板类型),提升了代码可读性与维护性,同时避免了手动指定类型可能导致的错误。
码事漫谈
2025/07/12
1100
C++11 auto关键字:原理解析与使用指南
【C++新特性】C++17结构化绑定
另一个比较有意思的使用地方在于可以增加代码可读性,例如输出map中所有的键值对。map如下:
公众号guangcity
2020/10/30
7.7K0
C++17常用新特性(三)---结构化绑定
结构化绑定允许用一个对象的元素或成员同时实例化多个实体。文字说明可能显得苍白无力。下面用代码的方式来说明:
CPP开发前沿
2022/04/13
1.9K0
C++17常用新特性(三)---结构化绑定
​数组和C++ std::array详解
std::array是C++容器库提供的一个固定大小数组的容器。其与内置的数组相比,是一种更安全、更容易使用的数组类型。std::array在头文件<array>中定义,其声明如下:
艰默
2023/09/05
1K0
​数组和C++ std::array详解
现代C++之万能引用、完美转发、引用折叠(万字长文)
0.导语1.问题引入2.引入万能引用3.万能引用出现场合4.理解左值与右值4.1 精简版4.2 完整版4.3 生命周期延长4.4 生命周期延长应用5.区分万能引用6.表达式的左右值性与类型无关7.引用折叠和完美转发7.1 引用折叠之本质细节7.2 示例与使用7.3 std::move()与std::forward()源码剖析8.不要返回本地变量的引用9.总结10.补充
公众号guangcity
2019/12/30
7K0
现代C++之万能引用、完美转发、引用折叠(万字长文)
C++20新特性个人总结
concept乃重头戏之一,用于模板库的开发。功能类似于C#的泛型约束,但是比C#泛型约束更为强大。
用户7886150
2021/02/04
2.1K0
【Modern CPP】结构化绑定
C++17 引入了结构化绑定(Structured Binding)这一强大的特性,它提供了一种简洁的语法,用于从容器、元组、数组等数据结构中解包并绑定其元素到多个变量中。结构化绑定不仅可以提高代码的可读性和简洁性,还能使代码更加灵活和易于维护。
程序员的园
2024/07/18
1520
【Modern CPP】结构化绑定
C++一分钟之-C++17特性:结构化绑定
在C++17这一里程碑式的版本中,引入了许多令人兴奋的新特性,其中之一便是结构化绑定(Structured Binding)。这一特性极大地简化了从聚合类型(如std::tuple, std::array, 或自定义的结构体)中解构数据的过程,使得代码更加简洁、易读。本文将深入浅出地介绍结构化绑定的基本概念、常见应用场景、易错点及避免策略,并通过代码示例加以说明。
Jimaks
2024/06/27
3190
全面盘点17个C++17的高级特性
C++17是目前比较常用的版本之一,今天花时间来梳理一下17个重要特性,所有的特性也不止这么点。
公众号guangcity
2024/03/22
4.3K0
全面盘点17个C++17的高级特性
C++17 在业务代码中最好用的十个特性
作者:jinshang,腾讯 WXG 后台开发工程师 自从步入现代 C++时代开始,C++语言标准形成了三年一个版本的惯例:C++11 标志着现代 C++的开端,C++14 在 11 的基础上查缺补漏,并未加入许多新特性,而 C++17 作为 C++11 后的第一个大版本,标志着现代 C++逐渐走向成熟。WXG 编译器升级到 gcc7.5 已有一段时间,笔者所在项目组也已经将全部代码升级到 C++17。在使用了 C++17 一年多之后,笔者总结了 C++17 在业务代码中最好用的十个特性。 注 1:本文只
腾讯技术工程官方号
2022/05/25
2.9K0
C++17 在业务代码中最好用的十个特性
C++ 20 学习笔记1 --From BiliBili.com
CPP1、一个函数返回多个变量的方式:1、通过引用传递参数,函数内修改参数值后,函数外部自动改变;2、通过指针传递参数,比引用传参好的点是,可以传nullPtr;3、Tuple4、Pair5、std::array 取值麻烦,array.get<0>(sources);不晓得这个0参数具体含义,不直观;6、struct包装多个变量,return {x,y};即可将x,y的值返回给调用方。CPP2:template1、类似java \c#中的泛型2、template<typename T>;3、template
BrianLee
2023/02/11
5260
C++17常用新特性
每次C++版本的发布都会带来很多新的特性,C++17也不例外,虽然有很多期待的特性没有包含进来,但是新增的特性依然挡不住它独特的魅力。
CPP开发前沿
2021/11/16
2.5K0
C++17常用新特性
C++11常用的一部分新特性
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
有礼貌的灰绅士
2023/06/14
5070
C++11常用的一部分新特性
C++ 新增的 stl 容器实用方法,你知道几个?(文末赠送 C++20 书籍)
1 原位构造与容器的emplace系列函数 在介绍emplace和emplace_back方法之前,我们先看一段代码: #include <iostream> #include <list> class Test { public:     Test(int a, int b, int c)     {         ma = a;         mb = b;         mc = c;         std::cout << "Test constructed." << std::endl;
范蠡
2022/10/08
1.1K0
C++ 新增的 stl 容器实用方法,你知道几个?(文末赠送 C++20 书籍)
你理解模板型别推导【C++】的原理吗?
auto类别推导其实就是模板类别推导,只不过模板类别推导涉及模板、函数和形参,而auto和它们无关
用户9831583
2022/12/04
6270
【C++进阶篇】C++11新特性(上篇)
列表初始化 { } 几乎适用于任意数据类型,成为现代编码的推荐方式。合理利用其特性可以可显著提升可读性和健壮性。
熬夜学编程的小王
2025/06/10
760
【C++进阶篇】C++11新特性(上篇)
C++一分钟之-C++17特性:结构化绑定
在C++17这一里程碑式的版本中,引入了许多令人兴奋的新特性,其中之一便是结构化绑定(Structured Binding)。这一特性极大地简化了从聚合类型(如std::tuple, std::array, 或自定义的结构体)中解构数据的过程,使得代码更加简洁、易读。本文将深入浅出地介绍结构化绑定的基本概念、常见应用场景、易错点及避免策略,并通过代码示例加以说明。
Jimaks
2024/06/26
9980
【c++】一篇文章带你了解c++11的新特性&&c++11详解
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。
用户10925563
2024/08/06
3330
【c++】一篇文章带你了解c++11的新特性&&c++11详解
深入解析C++的auto自动类型推导
关键字auto在C++98中的语义是定义一个自动生命周期的变量,但因为定义的变量默认就是自动变量,因此这个关键字几乎没有人使用。于是C++标准委员会在C++11标准中改变了auto关键字的语义,使它变成一个类型占位符,允许在定义变量时不必明确写出确切的类型,让编译器在编译期间根据初始值自动推导出它的类型。这篇文章我们来解析auto自动类型推导的推导规则,以及使用auto有哪些优点,还有罗列出自C++11重新定义了auto的含义以后,在之后发布的C++14、C++17、C++20标准对auto的更新、增强的功能,以及auto有哪些使用限制。
爱分享
2024/04/11
5630
深入解析C++的auto自动类型推导
C++(STL):02---tuple容器
一、tuple的历史概述 Tuple是TR1引入的东西,它扩展了pair的概念,拥有任意数量的元素。在C++11标准之前,tuple最多带有10个类型不同的元素 C++11,tuple被重新定义,采用variadic template概念,被设计为可用于任意大小的异质集合 二、tuple概述 tuple与pair类似,也是一个模板。pair接受两个成员,tuple接受任意数目的成员 当我们希望将一些数据组合成单一对象时,tuple非常有用 tuple的实现 TR1标准时(C++11之前),tuple最多带有
用户3479834
2021/02/03
1.4K0
C++(STL):02---tuple容器
推荐阅读
相关推荐
C++11 auto关键字:原理解析与使用指南
更多 >
交个朋友
加入[后端] 腾讯云技术交流站
后端架构设计 高可用系统实现
加入腾讯云运维技术交流群
云平台运维技巧 分布式系统排障
加入[游戏服务器] 腾讯云官方交流站
游戏服运维小技巧 常见问题齐排查
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档