5. C++14:完成 C++11

依据大版本和小版本交替发布的计划,C++14 [du Toit 2014] 的目标是“完成 C++11”(§3.2);也就是说,接受 2009 年特性冻结后的好的想法,纠正最初大规模使用 C++11 标准时发现的问题。对这个有限目标而言,C++14 是成功的。

重要的是,它表明 WG21 可以按时交付标准。反过来,这也使得实现者能够按时交付。在 2014 年年底之前,三个主要的 C++ 实现者(Clang、GCC 和微软)提供了大多数人认为完整的特性。尽管并没有完美地符合标准,但人们基本上可以对所有的特性和特性组合进行实验。要能编译“用到所有高级特性”的库,还需要延后一些时间(对微软而言要到 2018 年),但对于大多数用户而言,对标准的符合程度足以满足实际使用。标准工作和实现工作已经紧密联系在一起。这给社区带来了很大的不同。

C++14 特性集可以概括为:

  • 二进制字面量,例如 0b1001000011110011
  • §5.1:数字分隔符——为了可读性,例如 0b1001'0000'1111'0011
  • §5.2:变量模板——参数化的常量和变量
  • §5.3:函数返回类型推导
  • §5.4:泛型 lambda 表达式
  • §5.5constexpr 函数中的局部变量
  • 移动捕获——例如 [p = move(ptr)] {/* ... */}; 将值移入 lambda 表达式
  • 按类型访问元组,例如 x = get<int>(t);
  • 标准库中的用户定义字面量,例如:10i"Hello"s10s3ms55us17ns

这些特性中的大多数都面临着两个问题:“很好,什么使你花了这么长的时间?”以及“谁需要这个?”我的印象是,每个新特性都有着重要的需求作为动机——即使该需求不是通用的。在 constexpr 函数中添加局部变量和泛型 lambda 表达式大大改善了人们的代码。

重要的是,从 C++11 升级到 C++14 是相对无痛的,没有 ABI 破坏。经历过从 C++98 到 C++11 这一大而困难的升级的人感到了惊喜:他们升级可以比预想还快,花费的精力也更少。

5.1 数字分隔符

奇怪的是,数字分隔符引起了最激烈的争论。Lawrence Crowl 反复提出了各种选项的分析 [Crowl 2013]。包括我在内的许多人都主张使用下划线作为分隔符(和好几种其他语言一样)。例如:

auto a = 1_234_567;    // 1234567

不幸的是,人们正在使用下划线作为用户定义字面量后缀的一部分:

auto a = 1_234_567_s;  // 1234567 秒

这可能会引起歧义。例如,最后一个下划线是多余的分隔符还是后缀的开始?令我惊讶的是,这种潜在的歧义使下划线对很多人来说变得难以接受。其中一个原因是,为了免得程序员遇到意想不到的结果,库小组为标准库保留了不以下划线开头的后缀。经过长时间的讨论,包括全体委员会(约 100 人)的辩论,我们一致同意使用单引号:

auto a = 1'234'567;    // 1234567(整数)
auto b = 1'234'567s;   // 1234567 秒

尽管有严厉的警告指出使用单引号会破坏无数的工具,但实际效果似乎不错。单引号由 David Vandevoorde 提出 [Crowl et al. 2013]。他指出,在一些国家,特别是在瑞士的金融符号中,单引号被当作分隔符来使用。

我的另一个建议,使用空白字符,则一直没有得到认同:

int a = 1 234 567;     // 1234567
int b = 1 234 567 s;   // 1234567 秒

许多人认为这个建议是一个与在愚人节发表的老文章 [Stroustrup 1998] 有关的笑话。而实际上,它反映了一个旧规则,即相邻字符串会被连接在一起,因而 "abc" "def" 表示 "abcdef"

5.2 变量模板

2012 年,Gabriel Dos Reis 提议扩展模板机制,在模板类、函数和别名 [Dos Reis 2012] 之外加入模板变量。例如:

template<typename T>
constexpr T pi = T(3.1415926535897932385);

template<typename T>
T circular_area(T r)
{
    return pi<T> * r * r;
}

起初,我觉得这是一种平淡无奇的语言技术上的泛化,没有特别重要的意义。然而,为指定各种精度的常数而采取的变通办法由来已久,而且充斥着令人不安的变通和妥协。经过这种简单的语言泛化,代码可以大幅简化。特别是,变量模板作为定义概念的主要方式应运而生(§6.3.6)。例如:

// 表达式:
template<typename T>
concept SignedIntegral = Signed<T> && Integral<T>;

C++20 标准库提供了一组定义为变量模板的数学常数,最常见的情况是定义为 constexpr [Minkovsky and McFarlane 2019]。例如:

template<typename T> constexpr T pi_v = unspecified;
constexpr double pi = pi_v<double>;

5.3 函数返回类型推导

C++11 引入了从 lambda 表达式的 return 语句来推导其返回类型的特性。C++14 将该特性扩展到了函数:

template<typename T>
auto size(const T& a) { return a.size(); }

这种写法上的便利对于泛型代码中的小函数来说非常重要。但用户必须很小心,此类函数不能提供稳定的接口,因为它的类型现在取决于它的实现,而且在编译到使用这个函数的代码时,函数实现必须是可见的。

5.4 泛型 lambda 表达式

lambda 表达式是函数对象(§4.3.1),因此它们显然可以是模板。有关泛型(多态)lambda 表达式的问题在 C++11 的工作中已经进行了广泛讨论,但当时被认为还没有完全准备好(§4.3.1)。

2012 年,Faisal Vali、Herb Sutter 和 Dave Abrahams 提议了泛型 lambda 表达式 [Vali et al. 2012]。提议的写法只是从语法中省略了类型:

auto get_size = [](& m){ return m.size(); };

委员会中的许多人(包括我)都强烈反对,指出该语法太过特别,且不能推广到受约束的泛型 lambda 表达式中。因此,写法更改为使用 auto 作为标记,指明有类型需要推导:

auto get_size = [](auto& m){ return m.size(); };

这使泛型 lambda 表达式与早在 2002 年就提出的概念提案和泛型函数建议 [Stroustrup 2003; Stroustrup and Dos Reis 2003a,b] 保持一致。

这种将 lambda 表达式语法与语言其他部分所用的语法相结合的方向与一些人的努力背道而驰,这些人希望为泛型 lambda 表达式提供一种独特(超简洁)的语法,类似于其他语言 [Vali et al. 2012]:

C# 3.0 (2007):      x => x * x;
Java 1.8 (~2013):   x -> x * x;
D 2.0 (~2009):      (x) { return x * x; };

我认为,使用 auto 而且没有为 lambda 表达式引入特殊的不与函数共享的记法是正确的。此外,我认为在 C++14 中引入泛型 lambda 表达式,而没有引入概念,则是个错误;这样一来,对受约束和不受约束的 lambda 表达式参数和函数参数的规则和记法就没有一起考虑。由此产生的语言技术上的不规则(最终)在 C++20 中得到了补救(§6.4)。但是,我们现在有一代程序员习惯于使用不受约束的泛型 lambda 表达式并为此感到自豪,而克服这一点将花费大量时间。

从这里简短的讨论来看,似乎委员会流程对记法/语法给予了特大号的重视。可能是这样,但是语法并非无足轻重。语法是程序员的用户界面,与语法有关的争论通常反映了语义上的分歧,或者反映了对某一特性的预期用途。记法应反映基础的语义,而语法通常偏向于对某种用法(而非其他用法)有利。例如,一个完全通用和啰嗦的记法有利于希望表达细微差别的专家,而一个为表达简单情况而优化的记法,则有利于新手和普通用户。我通常站在后者这边,并且常常赞成两者同时都提供(§4.2)。

5.5 constexpr 函数中的局部变量

到 2012 年,人们不再害怕 constexpr 函数,并开始要求放松对其实现的限制。实际上有些人希望能够在 constexpr 函数中执行任何操作。但是,无论是使用者还是编译器实现者都还没有为此做好准备。

经过讨论,Richard Smith(谷歌)提出了一套相对适度的放松措施 [Smith 2013]。特别是,允许使用局部变量和 for 循环。例如:

constexpr int min(std::initializer_list<int> xs)
{
  int low = std::numeric_limits<int>::max();
  for (int x : xs)
    if (x < low)
      low = x;
  return low;
}

constexpr int m = min({1,3,2,4});

给定一个常量表达式作为参数,这个 min() 函数可以在编译时进行求值。本地的变量(此处为 lowx)仅在编译器中存在。计算不能对调用者的环境产生副作用。Gabriel Dos Reis 和 Bjarne Stroustrup 在原始的(学术)constexpr 论文中指出了这种可能性 [Dos Reis and Stroustrup 2010]。

这种放松简化了许多 constexpr 函数并使许多 C++ 程序员感到高兴。他们不满地发现,以前在编译时只能对算法的纯函数表达式进行求值。特别是,他们希望使用循环来避免递归。就更长期来看,这释放出了要在 C++17 和 C++20(§9.3.3)中进一步放松限制的需求。为了说明潜在的编译期求值的能力,我已经指出 constexpr thread 也是可能的,尽管我并不急于对此进行提案。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8