C++17:大海迷航 缺省比较 “太空船”运算符 统一调用语法 点运算符 网络库 条件的显式测试 文件系统 并行 STL

C++17:大海迷航

在经过 C++14 这个小版本标准之后,C++17 [Smith 2017] 原本被看作是一个大版本。C++17 有很多新的特性,但没有一个我认为称得上重大。尽管我们已经有给 C++11 和 C++14 带来成功的工作流程,标准社区也更丰富、更强大、更热情,但对于 C++17 的关键问题是:为什么所有的辛劳却没有带来更显著的改进?

C++17 作为一个重要的标准更新,确实引入了许多新的特性和改进,但相对于之前的 C++11 和 C++14,它可能没有带来那种一眼就能看到的“重大”改变。这其中的原因可能是多方面的。

首先,C++ 作为一个成熟且复杂的编程语言,其每一次标准的更新都需要经过深入的讨论和严谨的测试,以确保新特性的引入不会破坏现有的代码库或引入新的安全问题。因此,标准的制定过程本身就是一个相对缓慢且审慎的过程。

其次,C++17 的设计目标可能更注重于对现有特性的完善和优化,而不是引入全新的、颠覆性的特性。这并不意味着 C++17 的更新不重要,相反,它可能更加关注于提高代码的可读性、可维护性和性能。例如,C++17 引入了结构化绑定(structured bindings)、折叠表达式(fold expressions)、if 语句的初始化器(if with initializer)等特性,这些特性都极大地提高了代码的简洁性和可读性。

此外,C++ 社区在 C++17 的制定过程中可能更加注重于社区的反馈和意见,这可能导致一些原本计划中的特性因为争议较大而被推迟或修改。这种以社区为导向的开发方式虽然可能降低了某些特性的引入速度,但有助于确保新特性的广泛接受和稳定使用。

最后,对于是否称得上“重大”的改进,这本身就是一个主观的判断。对于不同的开发者来说,他们对“重大”的定义可能各不相同。有些人可能更看重性能的提升,有些人可能更关注于语法糖的增加,而有些人则可能更关心于库函数的扩展。因此,对于 C++17 是否带来了显著的改进,可能取决于个人的需求和期望。

总的来说,C++17 虽然没有带来那种颠覆性的“重大”改变,但它仍然是一个非常重要的标准更新,它进一步完善了 C++ 的特性集,提高了代码的可读性和可维护性,为开发者提供了更多的工具和选择。同时,我们也应该期待未来的 C++ 标准能够继续带来更多的创新和改进。

C++17 有大约 21 个新的语言特性(取决于你的计数方式),包括:

  • 构造函数模板参数推导——简化对象定义(§8.1
  • 推导指引——解决构造函数模板参数推导歧义的明确写法(§8.1
  • 结构化绑定——简化写法并消除一种未初始化变量的来源(§8.2
  • inline 变量——简化了那些仅有头文件的库实现中的静态分配变量的使用 [Finkel and Smith 2016]
  • 折叠表达式——简化变参模板的一些用法 [Sutton and Smith 2014]
  • 条件中的显式测试——有点像 for 语句中的条件(§8.7
  • 保证的复制消除——去除了很多不必要的拷贝操作 [Smith 2015]
  • 更严格的表达式求值顺序——防止了一些细微的求值顺序错误 [Dos Reis et al. 2016b]
  • auto 当作模板参数类型——值模板参数的类型推导 [Touton and Spertus 2016]
  • 捕捉常见错误的标准属性——[[maybe_unused]][[nodiscard]][[fallthrough]] [Tomazos 2015]
  • 十六进制浮点字面量 [Köppe 2016a]
  • 常量表达式 if——简化编译期求值的代码 [Voutilainen and Vandevoorde 2016]

不幸的是,这并不是完整的功能扩展列表。相当一部分是如此之小,我们很难简单地描述它们。

C++17 标准库中增加了大约 13 个新特性,并加上了许多小的修改:

  • optionalanyvariant——用于表达“可选”的标准库类型(§8.3
  • shared_mutexshared_lock(读写锁)和 scoped_lock§8.4
  • 并行 STL——标准库算法的多线程及矢量化版本(§8.5
  • 文件系统——可移植地操作文件系统路径和目录的能力(§8.6
  • string_view——对不可变字符序列的非所有权引用 [Yasskin 2014]
  • 数学特殊函数——包括拉盖尔和勒让德多项式、贝塔函数、黎曼泽塔函数 [Reverdy 2012]

尽管我也喜欢 C++17 中的某些功能,但令人困扰的是这些功能没有统一的主题,没有整体的规划,似乎只是由于可以达到投票多数而被扔进语言和标准库中的一组“聪明的想法”。这种状况可能给未来语言的发展带来更大的弊端,因此必须采取一些措施做出改变 [Stroustrup 2018d]。方向小组的成立是 WG21 针对这个问题的回应(§3.2)(§9.1)的一部分。

不可否认,C++17 提供了一些可以在小方面帮助大多数程序员的东西,但没有什么可以让我认为是重大的。在这里,我将“重大”定义为“对我们思考编程和组织代码的方式产生影响”。在此,我描述了我猜想会产生最大积极影响的功能。

我也检查了一些尽管经过严肃考虑、仍没有进入 C++17 标准的例子:

  • §6.3.8:概念(C++20)
  • §8.8.1:网络库
  • §8.8.2:点运算符(operator.()
  • §8.8.3:统一函数调用
  • §8.8.4:简单类型的默认比较运算符 ==!=<<=>>=
  • §9.3.2:协程(C++20)

我怀疑如果它们被采纳的话,其中的任何一项都会成为 C++17 最重要的特性之一。它们符合 C++ 应该成为什么的一致观点(§9.2);即使只有少数几项,也会极大地改变 C++17 的使用方式。

在 C++11 中我看到了相互支持的特性网,它们带来了更好的代码编写方式。对于 C++17,我没有看到。但是,C++20 完善了这样一张网,使 C++ 又向前迈进了一大步(§9)。可以说 C++17 只是通向 C++20 路上的垫脚石,但是委员会的讨论对此毫无暗示,重点始终放在单独的特性上。我甚至听到有人说“列车模型”(§3.2)不适合长期规划;事实并非如此。

8.1 构造函数模板参数推导

几十年来,人们好奇为什么模板参数可以从其他函数参数中推导出来,却不能从构造函数参数中推导。例如,在 C++98、C++11 和 C++14 中:

pair<string,int> p0 (string("Hi!"),129);  // 不需要推导
auto p1 = make_pair("Hi!"s,129);          // p1 是 pair<string,int>
pair p2 ("Hi!"s,129);    // 错误:pair 缺少模板参数

很自然,在我第一次设计模板的时候,我也考虑过从构造函数参数中推导出模板参数的可能性,但因为担心出现歧义而止步。解决方案也有技术障碍,但 Michael Spertus 和 Richard Smith 克服了这些障碍。所以在 C++17 中,我们可以写上面最后一个例子中那样的代码(p2)而不会报错,这样一来就不需要 make_pair() 了。

这简化了类型的使用,例如 pairtuple,还有当编写并行的代码时用到的锁和互斥锁(§8.4)。

shared_lock lck {
   m};    // 不需要显式写出锁类型

这是一个在 C++17 中少见的例子,相互支持的特性促成了明显的代码简化。不幸的是,这些简化被接受与否都是个案,而非总体的简化努力的结果。所以,在类型推导规则中“填坑”的努力仍在继续 [Spertus et al. 2018]。

除了这里的描述之外,这套机制提供了解决歧义的一种写法(§8.3)。

8.2 结构化绑定

结构化绑定始于 Herb Sutter、Bjarne Stroustrup 和 Gabriel Dos Reis 的一个简单的提案 [Sutter et al. 2015],旨在简化写法和消除剩余的几个变量未初始化的来源。例如:

template<typename T, typename U>
void print(vector<pair<T,U>>& v)
{
   
    for (auto [x,y] : v)
        cout << '{' << x << ' ' << y << "}\n";
}

名称 xy 被分别绑定于 pair 的第一个和第二个元素。这可算作是写法上的重大便利。

C++14 给我们提供了返回多个值的方便方式。例如:

tuple<T1,T2,T3> f(/*...*/)  // 优美的声明语法
{
   
    // ...
    return {
   a,b,c};  // 优美的返回语法
}

我认为在当前的 C++ 中,tuple 有点被过度使用了,当多个值并不互相独立的时候,我倾向于使用明确定义的类型,但从写法上讲,这没有什么区别。然而,C++14 并没有提供像创建多返回值那样方便的方式去解包它们。这导致了繁琐的变通解决方案、变量未初始化或运行期开销。例如:

tuple<T1,T2,T3> res = f();
T1& alpha = get<0>(res);    // 通过 alpha 来间接访问
T2& val = get<1>(res);
T3 err_code = get<2>(res);  // 拷贝

很多专家更喜欢用标准库函数 tie() 去解包 tuple

T1 x;
T2 y;
T3 z;
// ...
tie(x,y,z) = f(); // 使用现有变量的优美调用方式

tie() 函数赋值的时候,会向 tie() 函数的参数赋值。然而,使用 tie,你必须分别定义变量,并且写出它们的类型以匹配 f() 返回的对象的成员(在这个例子中就是 T1T2、和 T3)。不幸的是,这会导致局部变量“设置前使用”的错误,及“初始化后赋值”的开销。并且,大多数程序员并不知道 tie() 的存在,或者认为在真实代码中使用它太奇怪了。

Herb Sutter 建议了一种跟正常返回语法类似的方案:

auto {
   x,y,z} = f(); // 优美的调用语法,会引入别名

这对任何有三个成员的 struct 都有效,而不仅仅只对 tuple。消除核心指南(§10.6)中未初始化变量的倒数第二个来源是我的主要动机。是的,我喜欢这种写法,但更重要的是它使得 C++ 更接近于其理想。

不是每个人都喜欢这个想法,而且我们几乎没能在 C++17 中及时讨论

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EwenWanW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值