C++17结构化绑定

C++17结构化绑定

动机
std::map的insert办法回来std::pair,两个元素分别是指向所刺进键值对的迭代器与指示是否新刺进元素的布尔值,而std::map::iterator解引证又得到键值对std::pair。在一个触及std::map的算法中,有或许呈现很多的first和second,让人手足无措。

include

include

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

include

include

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

include

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 ;
其间,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为类元组(tuple-like)类型。在STL中,std::array、std::pair和std::tuple都是这样的类型。此刻,identifier-list的长度有必要与std::tuple_size::value持平,每个标识符的类型都经过std::tuple_element推导出(详细见后文),用成员get()或get(e)初始化。明显,这些规范库设备是与言语中心绑定的。

第三种状况,E对错union类类型,绑定非静态数据成员。一切非静态数据成员都有必要是public拜访特点,悉数在E中,或悉数在E的一个基类中(即不能涣散在多个类中)。identifier-list依照类中非静态数据成员的声明次序绑定,数量持平。

使用
结构化绑定拿手处理纯数据类型,包含自界说类型与std::tuple等,给实例的每一个字段分配一个变量名:

include

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

include

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

include

include

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

: std::integral_constant<std::size_t, 2> { };

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

template<>
struct tuple_element<1, Student> { using type = decltype(std::declval().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

include

include

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数组中元素的左值(但不是左值引证)——int array[2]{ 1, 2 }; auto& [i, j] = array; static_assert(!std::is_reference_v);;
类元组景象,假如e是左值引证,则e是左值(lvalue),不然是消亡值(xvalue);记Ti为std::tuple_element::type,则结构化绑定vi的类型是Ti的引证;当get回来左值引证时是左值引证,不然是右值引证;
数据成员景象,与数组相似,设数据成员mi被声明为Ti类型,则结构化绑定的类型是指向cv Ti的左值(相同不是左值引证)。
至此,我想“结构化绑定”的含义现已清晰了:标识符总是绑定一个目标,该目标是另一个目标的成员(或数组元素),后者或是复制或是引证(引证不是目标,意会即可)。与引证相似,结构化绑定都是既有目标的别号(这个目标或许是隐式的);与引证不同,结构化绑定纷歧定是引证类型。

(不理解的话能够参阅N4659 11.5节,虽然你很或许会愈加看不懂……)

现在能够解说ri非const的现象了:编译器先创建了变量const auto& e = tuple;,E为const std::tuple&,std::tuple_element<0, E>::type为int&,std::get<0>(e)相同回来int&,故ri为int&类型。

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

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

include

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没有value、explicit复制结构函数等,除非是深挖语法的language lawyer,在实践开发中不用纠结(上面这一堆现已能够算language lawyer了吧)。

限制
以上代码示例应该现已包含了一切类型的结构化绑定使用,你能幻想到的其他语法都是错的,包含但不限于:

用std::initializer_list初始化;
由于std::initializer_list的长度是动态的,但结构化绑定的标识符数量是静态的。

用列表初始化——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, T4> f(); auto [ w, [x, y], z ] = f();;
多写一行吧。[相同或许与attributes混杂。

以上语法都没有归入C++20规范,不过或许在将来成为C++语法的扩展

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

类模板参数推导(class template argument deduction,CTAD),由结构函数参数推导类模板参数;
复制消除,确保NRV(named return value)优化;
constexpr if,简化泛型代码,消除部分SFINAE;
带初始化的条件分支句子:语法糖,使代码愈加高雅。
原文地址https://www.cnblogs.com/jerry-fuyi/p/12892288.html