C++17结构化绑定
动机
std::map<K, V>的insert方法返回std::pair<iterator, bool>,两个元素分别是指向所插入键值对的迭代器与指示是否新插入元素的布尔值,而std::map<K, V>::iterator解引用又得到键值对std::pair<const K, V>。在一个涉及std::map的算法中,有可能出现大量的first和second,让人不知所措。
#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)为可选的attributes,cv-auto为可能有const或volatile修饰的auto,ref-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的类型E,std::tuple_size<E>是一个完整类型,则称E为类元组(tuple-like)类型。在STL中,std::array、std::pair和std::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是一个数据类型,有两个字段name和score。name是一个C风格字符串,它大概是从C代码继承来的,我希望客户能用上C++风格的std::string;score属于Transcript类型,表示学生的成绩单,这个结构比较大,我希望能传递const引用以避免不必要的拷贝。为此,我写明了三要素:std::tuple_size、std::tuple_element和get。这种机制给了结构化绑定很强的灵活性。
细节
#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;是合法的呢?
这个问题需要系统地从头谈起。先引入一个名字e,E为其类型:
- 当 - expression是数组类型- A,且- ref-operator不存在时,- E为- cv A,每个元素由- expression中的对应元素拷贝(- = expression)或直接初始化(- { expression }或- ( expression );
- 否则,相当于定义 - e为- attr cv-auto ref-operator e initializer;。
也就是说,方括号前面的修饰符都是作用于e的,而不是那些新声明的变量。至于为什么第一条会独立出来,这是因为在标准C++中第二条的形式不能用于数组拷贝。
然后分三种情况讨论:
- 数组情形, - E为- T的数组类型,则每个结构化绑定都是指向- 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);记- Ti为- std::tuple_element<i, E>::type,则结构化绑定- vi的类型是- Ti的引用;当- get返回左值引用时是左值引用,否则是右值引用;被引类型为- Ti;- —— - decltype对结构化绑定有特殊处理,产生被引类型,在类元组情形下结构化绑定的类型与被引类型是不同的;
- 数据成员情形,与数组类似,设数据成员 - mi被声明为- Ti类型,则结构化绑定的类型是指向- cv Ti的左值(同样不是左值引用);被引类型为- cv Ti。
至此,我想“结构化绑定”的意义已经明确了:标识符总是绑定一个对象,该对象是另一个对象的成员(或数组元素),后者或是拷贝或是引用(引用不是对象,意会即可)。与引用类似,结构化绑定都是既有对象的别名(这个对象可能是隐式的);与引用不同,结构化绑定不一定是引用类型。
(不理解的话可以参考N4659 11.5节,尽管你很可能会更加看不懂……)
现在可以解释ri非const的现象了:编译器先创建了变量const auto& e = tuple;,E为const std::tuple<int&>&,std::tuple_element<0, E>::type为int&,std::get<0>(e)同样返回int&,故ri为int&类型。
在面向底层的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>没有value、explicit拷贝构造函数等,除非是深挖语法的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;
- 带初始化的条件分支语句:语法糖,使代码更加优雅。 
C++17结构化绑定的更多相关文章
- C++17尝鲜:结构化绑定声明(Structured Binding Declaration)
		结构化绑定声明 结构化绑定声明,是指在一次声明中同时引入多个变量,同时绑定初始化表达式的各个子对象的语法形式. 结构化绑定声明使用auto来声明多个变量,所有变量都必须用中括号括起来. cv-auto ... 
- Solr系列四:Solr(solrj 、索引API 、 结构化数据导入)
		一.SolrJ介绍 1. SolrJ是什么? Solr提供的用于JAVA应用中访问solr服务API的客户端jar.在我们的应用中引入solrj: <dependency> <gro ... 
- seo之google rich-snippets丰富网页摘要结构化数据(微数据)实例代码
		seo之google rich-snippets丰富网页摘要结构化数据(微数据)实例代码 网页摘要是搜索引擎搜索结果下的几行字,用户能通过网页摘要迅速了解到网页的大概内容,传统的摘要是纯文字摘要,而结 ... 
- 页面结构化在 Android 上的尝试
		本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/M45DM5Ix7a2fmrsE8VPvxg 作者:b ... 
- Spark SQL结构化数据处理
		Spark SQL是Spark框架的重要组成部分, 主要用于结构化数据处理和对Spark数据执行类SQL的查询. DataFrame是一个分布式的,按照命名列的形式组织的数据集合. 一张SQL数据表可 ... 
- 你真的了解字典(Dictionary)吗?  C# Memory Cache 踩坑记录  .net 泛型  结构化CSS设计思维  WinForm POST上传与后台接收  高效实用的.NET开源项目  .net 笔试面试总结(3)  .net 笔试面试总结(2)  依赖注入  C# RSA 加密  C#与Java AES 加密解密
		你真的了解字典(Dictionary)吗? 从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ... 
- 妙味,结构化模块化 整站开发my100du
		********************************************************************* 重要:重新审视的相关知识 /* 妙味官网:www.miaov ... 
- Bigtable:一个分布式的结构化数据存储系统
		Bigtable:一个分布式的结构化数据存储系统 摘要 Bigtable是一个管理结构化数据的分布式存储系统,它被设计用来处理海量数据:分布在数千台通用服务器上的PB级的数据.Google的很多项目将 ... 
- shell的结构化命令
		shell在逻辑流程控制这里会根据设置的变量值的条件或其他命令的结果跳过一些命令或者循环执行的这些命令.这些命令通常称为结构化命令 1.if-then语句介绍 基本格式 if command then ... 
随机推荐
- Git把本地代码推送到远程github仓库
			运用Git版本控制系统进行代码的管理,以便于团队成员的协作,由于之前是使用svn来进行版本控制,所以对于Git使用还有待熟练掌握.Git与svn类似,个人认为两者之间比较直观的区别就是 Git 不需要 ... 
- 【题解】P2831 愤怒的小鸟 - 状压dp
			P2831愤怒的小鸟 题目描述 \(Kiana\) 最近沉迷于一款神奇的游戏无法自拔. 简单来说,这款游戏是在一个平面上进行的. 有一架弹弓位于 \((0,0)\) 处,每次 \(Kiana\) 可以 ... 
- SpringCloud-Hystrix 服务降级、熔断
			Hystrix 是什么? Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时.异常等,Hystrix 能够保证在一个依赖出问题的情况下 ... 
- Java 多线程--ThreadLocal Timer ExecutorService
			ThreadLocal /** * ThreadLocal:每个线程自身的存储本地.局部区域 * @author xzlf * */ public class ThreadLocalTest01 { ... 
- linux  批量删除文件
			来源;https://www.cnblogs.com/sinpo/p/7106998.html linux下批量删除文件 1. 在linux批量删除多级目录下同一格式的文件,可采用find + e ... 
- linux uniq 命令实用手册
			Linux uniq 命令用于处理文本内容中的重复行. 这里我们只介绍其常用参数,其完整用法可参见man uniq. 例如,我们有如下文件内容: >>> cat log.txt __ ... 
- 【Django】runserver 0.0.0.0:0 后,究竟发生了什么
			WSGI协议 Django是遵循WSGI协议设计的 WSGI协议主要包括server和application两个部分: WSGI server:负责从客户端接收请求,将request转发给applic ... 
- Tomcat系列教材 (一)- 教程
			Tomcat系列教材 (一)- 教程 Tomcat是常见的免费的web服务器. Tomcat 这个名字的来历,Tomcat是一种野外的猫科动物,不依赖人类,独立生活. Tomcat的作者,取这个名字的 ... 
- Spring IOC 之注册解析的 BeanDefinition
			2019独角兽企业重金招聘Python工程师标准>>> DefaultBeanDefinitionDocumentReader.processBeanDefinition() 完成 ... 
- 常用的CSS小技巧
			实际开发过程中会遇到一些需要用CSS小技巧处理的布局问题,现在分享几个个人工作中遇到的小问题和解决方案. 1.inline元素间的空白间隙 这里要介绍一个神器font-size:0. 如果你写了个列表 ... 
