C++17 详解 8
本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。
3 通用语言特性
完成了语言修复和阐明的章节后,现在我们准备好浏览其它广泛传播的特性了。本章中描述的改进也有可能使你的代码更加紧凑和富有表现力。
比如,通过结构化绑定,你可以通过更简单的语法使用元组(和类似元组的表达式)。这种在像 Python 这样的其它语言中很容易的东西,现在也可以在 C++ 里实现了!
本章你将学到:
- 结构化绑定/分解式声明(译注:原文为 decomposition declaration)
- 如何为你的自定义类提供结构化绑定接口
- 带初始化语句的
if
/switch
- 内联变量及其对 header-only 库的影响
- lambda 表达式可应用于
constexpr
上下文 - 嵌套命名空间的简化用法
3.1 结构化绑定声明
你经常用到 tuple
或是 pair
吗?
如果没有,你可能要开始了解这两种轻巧易用的类型了。tuple
能让你捆绑具有良好库支持的临时数据,而不用为所有内容创建自己的类型或使用传出参数。像结构化绑定这样的语言支持使它们(译注:相比自定义类型)更易于处理。
假设有一个返回由两个结果组成的 pair
的函数:
1 | std::pair<int, bool> InsertElement(int el) { ... } |
你可以通过 auto ret = InsertElement(...)
然后引用 ret.first
或 ret.second
的方式使用。或者你可以利用 std::tie
,它可以把 tuple
/pair
解包到自定义变量:
1 | int index { 0 }; |
上边的代码可能在你使用 std::set::insert
返回 std::pair
时有用:
1 | std::set<int> mySet; |
C++17 里,上边的代码可以变得更紧凑:
1 | std::set<int> mySet; |
现在,不需要 pair.first
和 pair.second
了,你可以使用有具体名字的两个变量代替。而且你还只用了一行代码而不是三行,这样更易读了。代码也更安全了,因为 iter
和 inserted
是通过表达式初始化的。
上边这种语法被叫做结构化绑定表达式(structured binding expression)。
3.1.1 语法
基本语法如下:
1 | auto [a, b, c, ...] = expression; |
编译器将 a, b, c, ...
列表中的所有标识符作为所在作用域内的变量名,并将它们绑定到表达式所指代对象的子对象或元素上。
在幕后,编译器可能生成如下伪代码:
1 | auto tempTuple = expression; |
译注:注意,
a b c
的声明方式均是using
。相比auto a = tempTuple.first;
这种重新定义行为,少了一次拷贝操作,且少占用了一个a
类型尺寸大小的内存。很精妙!
概念上来说,表达式结果被拷贝到一个类 tuple
对象(tempTuple
)里,其成员变量通过 a, b, c
暴露。但是,变量 a, b, c
不是引用,它们是隐含对象(译注:即临时对象 tempTuple
)的成员变量的别名(或绑定)。此临时对象会由编译器分配一个唯一名字。
比如:
1 | std::pair a(0, 1.0f); |
x
绑定了一个 int
对象,其值存储于 a
的一个隐含的拷贝对象中。同理,y
绑定到 float
对象。
修饰符
多种修饰符都可以用在结构化绑定里:
const
:
1 | const auto [a, b, c, ...] = expression; |
引用:
1 | auto& [a, b, c, ...] = expression; |
比如:
1 | std::pair a(0, 1.0f); |
上边的例子里,x
绑定到了 a
的一个隐含的引用对象的元素上。
同样,现在也很容易获得一个 tuple
成员的引用:
1 | auto& [ refA, refB, refC, refD ] = myTuple; |
[[attribute]]
同样适用:
1 | [[maybe_unused]] auto& [a, b, c, ...] = expression; |
译注:
[[maybe_unused]]
这个就是标准化的属性,后边有专门的章节介绍。
结构化绑定还是分解式声明?
对本特性,你可能听过另一个名字——分解式声明(译注:原文为 decomposition declaration)。标准化过程中这两个名字都有考虑过,最终采用了结构化绑定的版本。
结构化绑定不能被声明为 constexpr
有一个限制值得被记住。
1 | constexpr auto [x, y] = std::pair(0, 0); |
这会导致错误:
1 | error: structured binding declaration cannot be 'constexpr' |
此限制有可能在 C++20 通过提案 P1481 被解决。
3.1.2 绑定
结构化绑定不只限于 tuple
类型,有三种情况下也可以从中进行绑定:
- 如果初始值是一个数组:
1 | // works with arrays: |
这种情况下数组被拷贝到一个临时对象,a b c
分别指向临时对象的元素。
标识符个数必须同数组元素数量相等。
- 如果初始值支持
std::tuple_size<>
,并且提供了get<N>()
和std::tuple_element
函数:
1 | std::pair myPair(0, 1.0f); |
上边的片段里我们绑定了 myPair
。这意味着你同样可以为你自己的类提供结构化绑定服务,前提是你添加了 get<N>
接口实现。参考稍后小节里的例子。
- 如果初始值类型只包含非静态、公有成员:
1 | struct Point { |
x
和 y
分别指向 Point
结构体中的 Point::a
和 Point::b
。
自定义类不必是 POD(译注:Plain Old Data),但是标识符的数量必须等于非静态数据成员的数量。
注:C++17 里,你可以通过结构化绑定来绑定类的成员,只要它们是公有的。但当你想(通过结构化绑定)访问类的私有成员时就有问题了。C++20 里此问题得到了修复。参考 P0969R0。
3.1.3 示例
最酷的用例之一——在 range based for 循环里绑定:
1 | std::map<KeyType, ValueType> myMap; |
上边的例子里,我们绑定到了 [key, val]
键值对,从而可以在循环体中使用这些变量名称。在 C++17 之前,你必须操作 map
的迭代器——即一个 pair<first, second>
。使用 key/value
的实际名字让代码变得更有表现力。
上述技巧可以被用于:
1 |
|
循环体内,你可以安全地使用变量 city
和 population
。
为自定义类提供结构化绑定接口
如之前提到的,你可以为自定义类提供结构化绑定支持。
为此,你必须定义对应类型的 get<N>
,std::tuple_size
和 std::tuple_element
的特化版本。
比如,如果你有一个有三个成员的类,但是你只想暴露其公有接口:
1 | class UserEntry { |
结构化绑定需要的接口:
1 | // 利用 if constexpr: |
tuple_size
指明可获取的字段的数量,tuple_element
定义了指定元素的类型,get<N>
返回元素的值。
或者你可以显式地特化 get<>
代替 if constexpr
:
1 | template<> string get<0>(const UserEntry &u) { return u.GetName(); } |
对很多类型,写两个(或多个)函数可能比 if constexpr
版本更简单。
译注:也更合理,符合单一职责原则和开闭原则。
现在,你可以通过结构化绑定使用 UserEntry
了,比如:
1 | UserEntry u; |
本例只允许对类的读访问。如果你想要写入,类应该提供返回成员引用的访问函数,然后你要修改 get
的实现以支持引用。
如你所见,我们用到了 if constexpr
来实现 get<N>
,后边有对 if constexpr
介绍的章节。