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。

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

现在可以解释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 结构化绑定内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • visual studio 2019编译c++17的方法

    右键点击你的项目打开 属性 > C/C++ > Language > C++ Language Standard,选择c++ 17 或者 c++ latest即可. 步骤1:右键点击项目如下图: 步骤二,选择最下面的"属性"项,如下图: 步骤三,点击"配置属性",选择"通用",然后在右侧选择"C++语言标准",后面的下拉列表中选择自己需要的C++版本编译器即可. 到此这篇关于visual studio 201

  • C++17新特性个人总结

    C++17 编译器版本:GCC 7.1.Clang 5.0 __cplusplus:201703L 编译选项:-std=c++17 1 关键字 1.1 constexpr 扩展constexpr使用范围,可用于if语句中,也可用于lambda表达式中. 例子1: #include<iostream> template<bool ok> constexpr void foo() { //在编译期进行判断,if和else语句不生成代码 if constexpr (ok == true)

  • 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 <m

  • C++20中的结构化绑定类型示例详解

    目录 C++20中新增了一个非常有用的特性 结构化绑定概念 结构化绑定类型 数组 Pair 结构体 实现一个可以被结构化绑定的类元组类型 C++20中新增了一个非常有用的特性 结构化绑定(Structured Binding).它可以让我们方便地从一个容器类型中取出元素并绑定到对应的变量中,使得代码更加简洁.易读.接下来,本文将分别介绍结构化绑定的概念.类型以及如何实现一个可以被结构化绑定的类元组类型. 结构化绑定概念 结构化绑定是C++20中的一个语言特性,允许将一个结构体或者其他类似类型的容

  • HTML结构化:实践DIV+CSS网页布局入门指南

    你正在学习CSS布局吗?是不是还不能完全掌握纯CSS布局?通常有两种情况阻碍你的学习: 第一种可能是你还没有理解CSS处理页面的原理.在你考虑你的页面整体表现效果前,你应当先考虑内容的语义和结构,然后再针对语义.结构添加CSS.这篇文章将告诉你应该怎样把HTML结构化. 另一种原因是你对那些非常熟悉的表现层属性(例如:cellpadding,.hspace.align="left"等等)束手无策,不知道该转换成对 应的什么CSS语句. 当你解决了第一种问题,知道了如何结构化你的HTML

  • python爬虫获取小区经纬度以及结构化地址

    本文实例为大家分享了python爬虫获取小区经纬度.地址的具体代码,供大家参考,具体内容如下 通过小区名称利用百度api可以获取小区的地址以及经纬度,但是由于api返回的值中的地址形式不同,所以可以首先利用小区名称进行一轮爬虫,获取小区的经纬度,然后再利用经纬度Reverse到小区的结构化的地址.另外小区名称如果是'...号',可以在爬虫开始之前在'号'之后加一个'院',得到的精确度更高.这次写到程序更加便于二次利用,只需要给程序传递一个dataframe就可以坐等结果了.现在程序已经写好了,就

  • Python调用graphviz绘制结构化图形网络示例

    首先要下载:Graphviz - Graph Visualization Software 安装完成后将安装目录的bin 路径加到系统路径中,有时候需要重启电脑. 然后: pip install graphviz import graphviz as gz 有向图 dot = gz.Digraph() dot.node('1', 'Test1') dot.node('2', 'Test2') dot.node('3', 'Test3') dot.node('4', 'Test4') dot.ed

  • .net core日志结构化

    目录 前言 什么是结构化呢? 结构化,就是将原本没有规律的东西进行有规律话. 就比如我们学习数据结构,需要学习排序然后又要学习查询,说白了这就是一套,没有排序,谈如何查询是没有意义的,因为查询算法就是根据某种规律得到最佳的效果. 同样日志结构话,能够让我们得到一些好处.如果说容易检索,容易分析,总的来说就是让我们的日志更加有规律. 如果我们的日志结构化了,那么可以使用elasticsearch 这样的框架进行二次整理,再借助一些分析工具. 我们就能做到可视化分析系统的运行情况,做到日志告警.上下

  • 深入了解C语言结构化的程序设计

    目录 C语言是结构化的程序设计语言! if语句 1:单分支结构 2:多分支结构 悬空else while循环 总结 C语言是结构化的程序设计语言! 结构有三大类:顺序结构,选择结构,循环结构! 顺序结构:顺序结构是最简单的,只要按照解决问题的顺序写出相应的语句就行,它的执行顺序是自上而下,依次执行. 举个例子: #include<stdio.h> int main() { int i=1;//这段代码中i依次为1,2,3 printf("%d\n",i);//同时打印1,2

  • 解决Java 结构化数据处理开源库 SPL的问题

    目录 前言介绍: 1. 集合运算能力 2.Lambda语法 3. 在Lambda语法中直接引用字段 4. 动态数据结构 5. 解释型语言 前言介绍: 现代Java应用架构越来越强调数据存储和处理分离,以获得更好的可维护性.可扩展性以及可移植性,比如火热的微服务就是一种典型.这种架构通常要求业务逻辑要在Java程序中实现,而不是像传统应用架构中放在数据库中. 应用中的业务逻辑大都会涉及结构化数据处理.数据库(SQL)中对这类任务有较丰富的支持,可以相对简易地实现业务逻辑.但Java却一直缺乏这类基

  • python time模块时间戳 与 结构化时间详解

    目录 time模块 1. 时间戳 1.1 time.time() 1.2 时间戳 转 字符串 2. 结构化时间 2.1 本地时间 2.2 格林威治时间(零时区) 2.3 结构化时间 转 字符串 2.4 字符串 转 结构化时间 2.5 结构化数据的属性 2.6 结构化时间 转 时间戳 time模块 1:概述 时间表示的分类 时间戳 格式化的时间字符串 结构化时间 时间戳:时间戳表示的是从1970年1月1日整0点到目前秒的偏移量,数据类型是浮点型,主要用来让计算机看的 格式化的时间字符串:如 201

  • Python time模块之时间戳与结构化时间的使用

    目录 1. 时间戳 1.1 time.time() 1.2 时间戳 转 字符串 2. 结构化时间 2.1 本地时间 2.2 格林威治时间(零时区) 2.3 结构化时间 转 字符串 2.4 字符串 转 结构化时间 2.5 结构化数据的属性 2.6 结构化时间 转 时间戳 导入相关库 import time 1. 时间戳 1.1 time.time() time.time()可以得到的是 时间戳 .即 1970年1月1日0时0分0秒到现在时间的偏移量 s t1 = time.time() print

随机推荐