动机

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

#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是一个占位符,所在位置的赋值被忽略。

#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)。

#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),后来又被改回结构化绑定。这个名字想强调的是,结构化绑定的意义重在绑定而非声明。

语法

结构化绑定有三种语法:

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)为可选的attributescv-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等,给实例的每一个字段分配一个变量名:

#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++代码可以很优雅:

#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;
}

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

#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。这种机制给了结构化绑定很强的灵活性。

细节

#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++中不存在位域的指针和引用,但结构化绑定可以是指向位域的左值:

#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();
}

程序输出:

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的新特性不是孤立的,与结构化绑定相关的有:

C++17结构化绑定的更多相关文章

  1. C++17尝鲜:结构化绑定声明(Structured Binding Declaration)

    结构化绑定声明 结构化绑定声明,是指在一次声明中同时引入多个变量,同时绑定初始化表达式的各个子对象的语法形式. 结构化绑定声明使用auto来声明多个变量,所有变量都必须用中括号括起来. cv-auto ...

  2. Solr系列四:Solr(solrj 、索引API 、 结构化数据导入)

    一.SolrJ介绍 1. SolrJ是什么? Solr提供的用于JAVA应用中访问solr服务API的客户端jar.在我们的应用中引入solrj: <dependency> <gro ...

  3. seo之google rich-snippets丰富网页摘要结构化数据(微数据)实例代码

    seo之google rich-snippets丰富网页摘要结构化数据(微数据)实例代码 网页摘要是搜索引擎搜索结果下的几行字,用户能通过网页摘要迅速了解到网页的大概内容,传统的摘要是纯文字摘要,而结 ...

  4. 页面结构化在 Android 上的尝试

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/M45DM5Ix7a2fmrsE8VPvxg 作者:b ...

  5. Spark SQL结构化数据处理

    Spark SQL是Spark框架的重要组成部分, 主要用于结构化数据处理和对Spark数据执行类SQL的查询. DataFrame是一个分布式的,按照命名列的形式组织的数据集合. 一张SQL数据表可 ...

  6. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  7. 妙味,结构化模块化 整站开发my100du

    ********************************************************************* 重要:重新审视的相关知识 /* 妙味官网:www.miaov ...

  8. Bigtable:一个分布式的结构化数据存储系统

    Bigtable:一个分布式的结构化数据存储系统 摘要 Bigtable是一个管理结构化数据的分布式存储系统,它被设计用来处理海量数据:分布在数千台通用服务器上的PB级的数据.Google的很多项目将 ...

  9. shell的结构化命令

    shell在逻辑流程控制这里会根据设置的变量值的条件或其他命令的结果跳过一些命令或者循环执行的这些命令.这些命令通常称为结构化命令 1.if-then语句介绍 基本格式 if command then ...

随机推荐

  1. gridview 合并单元格后,选中颜色重新绘制

    gv_docargo.RowStyle += OnRowStyle; private void OnRowStyle(object sender, DevExpress.XtraGrid.Views. ...

  2. Java集合linkdList

    LinkedList特有功能: A:添加功能 public void addFitst(Object e) public void addLast(Object e) B:获取功能 public Ob ...

  3. POJ3460 Booksort

    飞来山上千寻塔,闻说鸡鸣见日升. 不畏浮云遮望眼,自缘身在最高层.--王安石 题目:Booksort 网址:http://poj.org/problem?id=3460 Description The ...

  4. eclipse 创建maven项目失败

    问题描述: eclipse 初次创建maven项目报错 可能是maven-archetype-quickstart:1.1.jar 包失效了或者没有? 有人说把这个jar包放在maven本地仓库里 我 ...

  5. 【Linux常见问题】CentOS 7 root用户密码忘记,找回密码方法

    1.开机按esc 2.选择CentOS Linux (3.10.0-693.......)     按 e 键: 3.光标移动到 linux 16 开头的行,找到 ro 改为 rw init=sysr ...

  6. BootStrap的栅格式布局

    1.栅格系统(布局) Bootstrap内置了一套响应式.移动设备优先的流式栅格系统,随着屏幕设备或视口(viewport)尺寸的增加,系统会自动分为最多12列. 我在这里是把Bootstrap中的栅 ...

  7. 微服务为什么一定要用docker

    引言 早在2013年的时候,docker就已经发行,然而那会还是很少人了解docker.一直到2014年,Martin Fowler提出了微服务的概念,两个不相干的技术终于走在了一起,创造了今天的辉煌 ...

  8. Visual Studio Code插件安装步骤

    1.进入扩展视图视图安装或卸载(快捷键Ctrl+shift+x) 转载于:https://www.cnblogs.com/SakalakaZ/p/7725159.html

  9. POJ - 2387 Til the Cows Come Home (最短路入门)

    Bessie is out in the field and wants to get back to the barn to get as much sleep as possible before ...

  10. 洛谷 P1816 忠诚 ST函数

    题目描述 老管家是一个聪明能干的人.他为财主工作了整整10年,财主为了让自已账目更加清楚.要求管家每天记k次账,由于管家聪明能干,因而管家总是让财主十分满意.但是由于一些人的挑拨,财主还是对管家产生了 ...