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.firstret.second 的方式使用。或者你可以利用 std::tie,它可以把 tuple/pair 解包到自定义变量:

1
2
3
int index { 0 };
bool flag { false };
std::tie(index, flag) = InsertElement(10);

上边的代码可能在你使用 std::set::insert 返回 std::pair 时有用:

1
2
3
4
5
6
7
8
std::set<int> mySet;
std::set<int>::iterator iter;
bool inserted;

std::tie(iter, inserted) = mySet.insert(10);

if (inserted)
std::cout << "Value was inserted\n";

C++17 里,上边的代码可以变得更紧凑:

1
2
3
std::set<int> mySet;

auto [iter, inserted] = mySet.insert(10);

现在,不需要 pair.firstpair.second 了,你可以使用有具体名字的两个变量代替。而且你还只用了一行代码而不是三行,这样更易读了。代码也更安全了,因为 iterinserted 是通过表达式初始化的。

上边这种语法被叫做结构化绑定表达式(structured binding expression)。

3.1.1 语法

基本语法如下:

1
2
3
auto [a, b, c, ...] = expression;
auto [a, b, c, ...] { expression };
auto [a, b, c, ...] ( expression );

编译器将 a, b, c, ... 列表中的所有标识符作为所在作用域内的变量名,并将它们绑定到表达式所指代对象的子对象或元素上。

在幕后,编译器可能生成如下伪代码:

1
2
3
4
auto tempTuple = expression;
using a = tempTuple.first;
using b = tempTuple.second;
using c = tempTuple.third;

译注:注意,a b c 的声明方式均是 using。相比 auto a = tempTuple.first; 这种重新定义行为,少了一次拷贝操作,且少占用了一个 a 类型尺寸大小的内存。很精妙!

概念上来说,表达式结果被拷贝到一个类 tuple 对象(tempTuple)里,其成员变量通过 a, b, c 暴露。但是,变量 a, b, c 不是引用,它们是隐含对象(译注:即临时对象 tempTuple)的成员变量的别名(或绑定)。此临时对象会由编译器分配一个唯一名字。

比如:

1
2
std::pair a(0, 1.0f);
auto [x, y] = a;

x 绑定了一个 int 对象,其值存储于 a 的一个隐含的拷贝对象中。同理,y 绑定到 float 对象。

修饰符

多种修饰符都可以用在结构化绑定里:

const

1
const auto [a, b, c, ...] = expression;

引用:

1
2
auto& [a, b, c, ...] = expression;
auto&& [a, b, c, ...] = expression;

比如:

1
2
3
4
std::pair a(0, 1.0f);
auto& [x, y] = a;
x = 10; // 写访问
//a.first 现在等于 10

上边的例子里,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. 如果初始值是一个数组:
1
2
3
// works with arrays:
double myArray[3] = { 1.0, 2.0, 3.0 };
auto [a, b, c] = myArray;

这种情况下数组被拷贝到一个临时对象,a b c 分别指向临时对象的元素。

标识符个数必须同数组元素数量相等。

  1. 如果初始值支持 std::tuple_size<>,并且提供了 get<N>()std::tuple_element 函数:
1
2
std::pair myPair(0, 1.0f);
auto [a, b] = myPair; // 绑定 myPair.first/second

上边的片段里我们绑定了 myPair。这意味着你同样可以为你自己的类提供结构化绑定服务,前提是你添加了 get<N> 接口实现。参考稍后小节里的例子。

  1. 如果初始值类型只包含非静态、公有成员:
1
2
3
4
5
6
7
8
9
10
struct Point {
double x;
double y;
};

Point GetStartPoint() {
return { 0.0, 0.0 };
}

const auto [x, y] = GetStartPoint();

xy 分别指向 Point 结构体中的 Point::aPoint::b

自定义类不必是 POD(译注:Plain Old Data),但是标识符的数量必须等于非静态数据成员的数量。

注:C++17 里,你可以通过结构化绑定来绑定类的成员,只要它们是公有的。但当你想(通过结构化绑定)访问类的私有成员时就有问题了。C++20 里此问题得到了修复。参考 P0969R0

3.1.3 示例

最酷的用例之一——在 range based for 循环里绑定:

1
2
3
4
5
std::map<KeyType, ValueType> myMap;
for (const auto & [key,val] : myMap)
{
// 使用 key/value 而不是 iter.first/iter.second
}

上边的例子里,我们绑定到了 [key, val] 键值对,从而可以在循环体中使用这些变量名称。在 C++17 之前,你必须操作 map 的迭代器——即一个 pair<first, second>。使用 key/value 的实际名字让代码变得更有表现力。

上述技巧可以被用于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <map>
#include <iostream>
#include <string>

int main()
{
const std::map<std::string, int> mapCityPopulation {
{ "Beijing", 21'707'000 },
{ "London", 8'787'892 },
{ "New York", 8'622'698 }
};

for (auto&[city, population] : mapCityPopulation)
std::cout << city << ": " << population << '\n';
}

循环体内,你可以安全地使用变量 citypopulation

为自定义类提供结构化绑定接口

如之前提到的,你可以为自定义类提供结构化绑定支持。

为此,你必须定义对应类型的 get<N>std::tuple_sizestd::tuple_element 的特化版本。

比如,如果你有一个有三个成员的类,但是你只想暴露其公有接口:

1
2
3
4
5
6
7
8
9
10
11
class UserEntry {
public:
void Load() { }

std::string GetName() const { return name; }
unsigned GetAge() const { return age; }
private:
std::string name;
unsigned age { 0 };
size_t cacheEntry { 0 }; // 不暴露
};

结构化绑定需要的接口:

1
2
3
4
5
6
7
8
9
10
11
12
// 利用 if constexpr:
template <size_t I> auto get(const UserEntry& u) {
if constexpr (I == 0) return u.GetName();
else if constexpr (I == 1) return u.GetAge();
}

namespace std {
template <> struct tuple_size<UserEntry> : std::integral_constant<size_t, 2> { };

template <> struct tuple_element<0,UserEntry> { using type = std::string; };
template <> struct tuple_element<1,UserEntry> { using type = unsigned; };
}

tuple_size 指明可获取的字段的数量,tuple_element 定义了指定元素的类型,get<N> 返回元素的值。

或者你可以显式地特化 get<> 代替 if constexpr

1
2
template<> string get<0>(const UserEntry &u) { return u.GetName(); }
template<> unsigned get<1>(const UserEntry &u) { return u.GetAge(); }

对很多类型,写两个(或多个)函数可能比 if constexpr 版本更简单。

译注:也更合理,符合单一职责原则和开闭原则。

现在,你可以通过结构化绑定使用 UserEntry 了,比如:

1
2
3
4
UserEntry u;
u.Load();
auto [name, age] = u; // 读访问
std:: cout << name << ", " << age << '\n';

本例只允许对类的读访问。如果你想要写入,类应该提供返回成员引用的访问函数,然后你要修改 get 的实现以支持引用。

如你所见,我们用到了 if constexpr 来实现 get<N>,后边有对 if constexpr 介绍的章节。

扩展:本修改提案:P0217(措辞),P0144(论证和示例),P0615(“分解式声明”改名为“结构化绑定”)。

评论