C++ 17 相比 C++ 14 新增之特征

目录

[1. auto 变量可直接初始化](#1. auto 变量可直接初始化)

[2. 无消息的 static_assert](#2. 无消息的 static_assert)

[3. 允许使用 typename 替代 class 声明模板参数](#3. 允许使用 typename 替代 class 声明模板参数)

[4. 内嵌命名空间](#4. 内嵌命名空间)

[5. 允许枚举器和命名空间具有属性](#5. 允许枚举器和命名空间具有属性)

[6. UTF-8 字符文字量](#6. UTF-8 字符文字量)

[7. 允许对所有非类型模板参数进行常量求值](#7. 允许对所有非类型模板参数进行常量求值)

[8. 增加折叠表达式](#8. 增加折叠表达式)

[9. 增加一元折叠表达式和空参数包](#9. 增加一元折叠表达式和空参数包)

[10. 使异常规范成为类型系统的一部分](#10. 使异常规范成为类型系统的一部分)

[11. 使用基类对类进行聚合初始化](#11. 使用基类对类进行聚合初始化)

[12. 允许 lambda 对 *this 的捕获](#12. 允许 lambda 对 *this 的捕获)

[13. 无需重复地使用属性之命名空间](#13. 无需重复地使用属性之命名空间)

[14. 对齐数据动态分配内存](#14. 对齐数据动态分配内存)

[15. 预处理器条件中的 __has_include](#15. 预处理器条件中的 __has_include)

[16. 类模板之模板参数推断](#16. 类模板之模板参数推断)

[17. 带 auto 类型的非类型模板参数](#17. 带 auto 类型的非类型模板参数)

[18. 保证复制省略](#18. 保证复制省略)

[19. 继承构造函数的新规范(DR1941 等)](#19. 继承构造函数的新规范(DR1941 等))

[20. 枚举的直接列表初始化](#20. 枚举的直接列表初始化)

[21. 更严的表达式求值次序](#21. 更严的表达式求值次序)

[22. constexpr lambda 表达式](#22. constexpr lambda 表达式)

[23. 范围 for 中不同的 begin 和 end 类型](#23. 范围 for 中不同的 begin 和 end 类型)

[24. 新增属性 [[fallthrough]]](#24. 新增属性 [[fallthrough]])

[25. 新增属性 [[nodiscard]]](#25. 新增属性 [[nodiscard]])

[26. 新增属性 [[maybe_unused]]](#26. 新增属性 [[maybe_unused]])

[27. 忽略未知属性而不报错](#27. 忽略未知属性而不报错)

[28. 在 using 声明中使用打包扩展](#28. 在 using 声明中使用打包扩展)

[29. 新增结构化绑定声明](#29. 新增结构化绑定声明)

[30. 新增十六进制浮点文字量(字面量)](#30. 新增十六进制浮点文字量(字面量))

[31. if 和 switch 语句的初始化语句](#31. if 和 switch 语句的初始化语句)

[32. 允许内联变量](#32. 允许内联变量)

[33. 模板参数匹配排除兼容模板](#33. 模板参数匹配排除兼容模板)

[34. std::uncaught_exceptions()](#34. std::uncaught_exceptions())

[35. constexpr if 语句](#35. constexpr if 语句)

SFINAE:

[if constexpr](#if constexpr)


1. auto变量可直接初始化

cpp 复制代码
auto x = foo(); // 复制初始化
auto x{foo}; // 直接初始化, 初始化一个初始化列表
int x = foo(); // 复制初始化
int x{foo}; // 直接初始化

2. 无消息的 static_assert

顾名思义,它允许在不传递消息的情况下设置条件,带消息的版本也将提供。它将与其他断言(例如 BOOST_STATIC_ASSERT,该断言一开始就不接受任何消息)兼容。用于检测条件是否成立,在编译时检查,实际上有些编辑器在编辑时就能给出提示信息。

static_assert( 常量表达式, 文字消息);

static_assert( 常量表达式); // C++17 及其后版本

cpp 复制代码
constexpr int identity(int x) { return x; }
static_assert(identity(10) == 10, "expected the same value"); // C++11及其后版本
static_assert(identity(10) == 10); // 无消息, C++17 及其后版本

3. 允许使用 typename 替代 class 声明模板参数

允许你在声明模板参数时使用类型名(typename)而不是类名(class)。普通类型参数可以互换使用这两种类型名,但模板参数之前仅限于类名,因此这项更改在一定程度上统一了这两种形式。

cpp 复制代码
template <template <typename...> typename Container>
//过去是无效的
struct foo;

foo<std::vector> my_foo;

4. 内嵌命名空间

例如,可以写成

cpp 复制代码
namespace A::B::C {
   // ...
}

而不是写成

cpp 复制代码
namespace A {
    namespace B {
        namespace C {
            // ...
        }
    }
}

5. 允许枚举器和命名空间具有属性

例如:

cpp 复制代码
enum E {
  foobar = 0,
  foobat [[deprecated]] = foobar
};

E e = foobat; // 警告信息

namespace [[deprecated]] old_stuff{
    void legacy();
}

old_stuff::legacy(); // 警告信息

6. UTF-8 字符文字量

UTF-8 字符文字量(字面量),例如 u8'a'。此类字面量类型为 char,其值等于 ISO 10646 中 c-char 的码位值,前提是该码位值可以用单个 UTF-8 码单元表示。如果 c-char 不在 Basic Latin 或 C0 Controls Unicode 块中,则程序格式错误。

如果字符无法放入 u8 ASCII 范围,编译器将报告错误。

例如:

cpp 复制代码
#include <string>
using namespace std::string_literals; // enables s-suffix for std::string literals

#include <string>
using namespace std::string_literals; // enables s-suffix for std::string literals

int main()
{
    // Character literals
    auto c0 =   'A'; // char
    auto c1 = u8'A'; // char
    auto c2 =  L'A'; // wchar_t
    auto c3 =  u'A'; // char16_t
    auto c4 =  U'A'; // char32_t

    // Multicharacter literals
    auto m0 = 'abcd'; // int, value 0x61626364

    // String literals
    auto s0 =   "hello"; // const char*
    auto s1 = u8"hello"; // const char* before C++20, encoded as UTF-8,
                         // const char8_t* in C++20
    auto s2 =  L"hello"; // const wchar_t*
    auto s3 =  u"hello"; // const char16_t*, encoded as UTF-16
    auto s4 =  U"hello"; // const char32_t*, encoded as UTF-32

    // Raw string literals containing unescaped \ and "
    auto R0 =   R"("Hello \ world")"; // const char*
    auto R1 = u8R"("Hello \ world")"; // const char* before C++20, encoded as UTF-8,
                                      // const char8_t* in C++20
    auto R2 =  LR"("Hello \ world")"; // const wchar_t*
    auto R3 =  uR"("Hello \ world")"; // const char16_t*, encoded as UTF-16
    auto R4 =  UR"("Hello \ world")"; // const char32_t*, encoded as UTF-32

    // Combining string literals with standard s-suffix
    auto S0 =   "hello"s; // std::string
    auto S1 = u8"hello"s; // std::string before C++20, std::u8string in C++20
    auto S2 =  L"hello"s; // std::wstring
    auto S3 =  u"hello"s; // std::u16string
    auto S4 =  U"hello"s; // std::u32string

    // Combining raw string literals with standard s-suffix
    auto S5 =   R"("Hello \ world")"s; // std::string from a raw const char*
    auto S6 = u8R"("Hello \ world")"s; // std::string from a raw const char* before C++20, encoded as UTF-8,
                                       // std::u8string in C++20
    auto S7 =  LR"("Hello \ world")"s; // std::wstring from a raw const wchar_t*
    auto S8 =  uR"("Hello \ world")"s; // std::u16string from a raw const char16_t*, encoded as UTF-16
    auto S9 =  UR"("Hello \ world")"s; // std::u32string from a raw const char32_t*, encoded as UTF-32
}

7. 允许对所有非类型模板参数进行常量求值

移除指针、引用以及指向非类型模板参数成员的指针的语法限制:

例如:

cpp 复制代码
template<int *p> struct A {};
int n;
A<&n> a; // ok

constexpr int *p() { return &n; }
A<p()> b; // error before C++17

8. 增加折叠表达式

C++17 中的折叠表达式是一项强大的特性,它允许你对二元运算符上的参数包进行简化或"折叠"。引入折叠表达式的目的是为了简化操作可变参数模板的代码,使其更加简洁易读

例如:

cpp 复制代码
template<typename... Args>
auto SumWithOne(Args... args){
    return (1 + ... + args);
}

折叠表达式有 4 种类型,在二元折叠表达式中,两个运算符 op 必须相同:

(1) 一元右折叠(不定参数在运算符右侧)

复制代码
(pack op ...)

例如:

(2) 一元左折叠(不定参数在运算符左侧)

复制代码
(... op pack)

(3) 二元右折叠(不定参数在运算符右边)

复制代码
(pack op ... op init)

例如:

cpp 复制代码
#include <iostream>

template<typename... Args>
int sum(Args&&... args)
{
    return (args + ...); // 执行一个加的二元右折叠
}

int main()
{
    int result = sum(1, 2, 3, 4);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

输出:

复制代码
Result: 10

这里,求和函数使用加法运算符 + 进行二元左折叠来计算其所有参数的总和。注意,上述 && 是右值引用符号。

(4) 二元左折叠(不定参数在运算符左边)

复制代码
(init op ... op pack)
cpp 复制代码
#include <iostream>

template<typename... Args>
bool any(Args... args) {
    return (... || args);
}

int main() {
    bool b = any(false, false, false);
    std::cout << "Result: " << std::boolalpha << b << std::endl;
    return 0;
}

输出:

复制代码
Result: false

此示例演示了对空包进行二元左折叠的操作。结果为假,因为逻辑或运算符 || 对空包返回假。

9. 增加一元折叠表达式和空参数包

10. 使异常规范成为类型系统的一部分

以前,函数的异常规范不属于函数的类型,但现在它将成为函数类型的一部分。

例如,下述操作将报错:

cpp 复制代码
void (*p)();
void (**pp)() noexcept = &p;   // 错误: 不能将一个指针转化为 noexcept 函数

struct S { typedef void (*p)(); operator p(); };
void (*q)() noexcept = S();   // 错误: 不能将一个指针转化为 noexcept 函数

11. 使用基类对类进行聚合初始化

如果一个类派生自其他类型,则不能使用聚合初始化。但现在此限制已取消。

(注:顾名思义,就是将多个元素聚集在一起。这个定义包含了混合类型的聚合体,例如结构体和类。数组则是单一类型的聚合体。初始化聚合体可能容易出错且繁琐。C++ 的聚合体初始化机制使其更加安全。)

例如:

cpp 复制代码
struct base { int a1, a2; };
struct derived : base { int b1; };

derived d1{{1, 2}, 3};      // 完全显式初始化
derived d1{{}, 1};          //  基类默认初始化

总而言之:从标准来看:

聚合体(多种数据组合体)是一个数组或类,它满足以下条件:

* 没有用户提供的构造函数(包括从基类继承的构造函数),

* 没有私有或受保护的非静态数据成员(第 11 条),

* 没有基类(第 10 条)并且 // 现已移除!

* 没有虚函数(10.3 条),以及

* 没有虚基类、私有基类或受保护基类(10.1 条)。

12. 允许 lambda 对 *this 的捕获

成员函数内部的 lambda 表达式会隐式捕获此指针(如果你使用默认捕获方式,例如 [&] 或 [=])。成员变量始终通过此指针访问。

例如:

cpp 复制代码
struct S {
   int x ;
   void f() {
      // 以下 lambda 捕获是相同的。
      auto a = [&]() { x = 42 ; } // 可行: 转换为 (*this).x
      auto b = [=]() { x = 43 ; } // 可行: 转换为 (*this).x
      a();
      assert( x == 42 );
      b();
      assert( x == 43 );
   }
};

现在,你可以在声明 lambda 表达式时使用 *this,例如 auto b = [=, *this]() { x = 43 ; }。这样,this 就按值捕获了。注意,[&,this] 这种形式是冗余的,但为了与 ISO C++14 兼容,仍然被接受。

按值捕获对于异步调用和并行处理尤为重要。

13. 无需重复地使用属性之命名空间

简化了使用多个属性的情况,例如:

cpp 复制代码
void f() {
    [[rpr::kernel, rpr::target(cpu,gpu)]] // 重复
    do-task();
}

建议改为:

cpp 复制代码
void f() {
    [[using rpr: kernel, target(cpu,gpu)]]
    do-task();
}

这种简化可能有助于构建能够自动将带注释的此类代码转换为不同编程模型的工具。

14. 对齐数据动态分配内存

例如:

cpp 复制代码
class alignas(16) float4 {
    float f[4];
};
float4 *p = new float4[1000];

C++11/14 没有规定任何机制来动态地为对齐的数据正确分配内存(即,保持数据的对齐方式)。在上面的例子中,C++ 实现不仅不需要为数组分配正确对齐的内存,实际上,几乎必然会错误地分配内存。

C++17 通过引入使用 align 参数的额外内存分配函数来弥补这一缺陷:

cpp 复制代码
void* operator new(std::size_t, std::align_val_t);
void* operator new[](std::size_t, std::align_val_t);
void operator delete(void*, std::align_val_t);
void operator delete[](void*, std::align_val_t);
void operator delete(void*, std::size_t, std::align_val_t);
void operator delete[](void*, std::size_t, std::align_val_t);

15. 预处理器条件中的 __has_include

此功能允许 C++ 程序直接、可靠且可移植地判断库头文件是否可供包含。

示例:以下演示了仅在库可选功能可用时才使用它的方法。

cpp 复制代码
#if __has_include(<optional>)
#  include <optional>
#  define have_optional 1
#elif __has_include(<experimental/optional>)
#  include <experimental/optional>
#  define have_optional 1
#  define experimental_optional 1
#else
#  define have_optional 0
#endif

16. 类模板之模板参数推断

在 C++17 之前,模板推导适用于函数,但不适用于类。例如,以下代码是合法的:

cpp 复制代码
void f(std::pair<int, char>);

f(std::make_pair(42, 'z'));

因为 std::make_pair 是一个模板函数(所以我们可以进行模板推导)。但以下代码却不行:

cpp 复制代码
void f(std::pair<int, char>);

f(std::pair(42, 'z'));

虽然语义上等价,但这在当时是不合法的,因为 std::pair 是一个模板类,而模板类在初始化时不能进行类型推导

因此,在 C++17 之前,必须显式地写出类型,即使这样做并没有添加任何新信息。

cpp 复制代码
void f(std::pair<int, char>);

f(std::pair<int, char>(42, 'z'));

C++17 修复了这个问题,模板类构造函数可以推导出类型参数。因此,构造此类模板类的语法与构造非模板类的语法一致。

17. 带 auto 类型的非类型模板参数

自动推断非类型模板参数的类型。

cpp 复制代码
template <auto value> void f() { }
f<10>();               // deduces int

18. 保证复制省略

对临时对象省略复制,而非命名 RVO(Return Value Optimization)(编译器优化选项)。

例如:

cpp 复制代码
// based on P0135R0
struct NonMoveable 
{
  NonMoveable(int);
  // 无复制或移动构造函数:
  NonMoveable(const NonMoveable&) = delete;
  NonMoveable(NonMoveable&&) = delete;

  std::array<int, 1024> arr;
};

NonMoveable make() 
{
  return NonMoveable(42);
}

// 创建对象:
auto largeNonMovableObj = make();

19. 继承构造函数的新规范(DR1941 等)

更多描述和论证请参见 P0136R0。以下是部分摘录:

继承构造函数的行为与其他任何形式的 using 声明都不同。所有其他 using 声明都会使一组声明在其他上下文中可见,以便进行名称查找,但继承构造函数声明声明了一个新的构造函数,该构造函数仅仅是将调用委托给原始构造函数。

此功能改变了继承构造函数的声明方式,不再声明一组新的构造函数,而是将一组基类构造函数在派生类中可见,如同它们是派生类构造函数一样。(当使用此类构造函数时,额外的派生类子对象也会被隐式构造,如同使用默认构造函数一样。)换句话说:尽可能地使继承构造函数的行为与继承任何其他基类成员的行为完全相同。

此更改确实会影响某些程序的含义和有效性,但这些更改提高了 C++ 的一致性和可理解性。

cpp 复制代码
//隐藏操作与存在默认参数时其他成员使用 using 声明的操作相同。
struct A {
  A(int a, int b = 0);
  void f(int a, int b = 0);
};
struct B : A {
  B(int a);      using A::A;
  void f(int a); using A::f;
};
struct C : A {
  C(int a, int b = 0);      using A::A;
  void f(int a, int b = 0); using A::f;
};

B b(0); // 以前正确, 现在有歧义
b.f(0); // 同样有歧义(标准未变)

C c(0); // 以前有歧义, 现在正确
c.f(0); // 正确 (标准未变)

// 继承构造函数参数不再被复制
struct A { A(const A&) = delete; A(int); };
struct B { B(A); void f(A); };
struct C : B { using B::B; using B::f; };
C c({0}); // 以前格式错误,现在可以了(未创建副本)
c.f({0}); // 可行 (标准不变)

20. 枚举的直接列表初始化

允许使用固定的底层类型初始化枚举类:

cpp 复制代码
enum class Handle : uint32_t { Invalid = 0 };
Handle h { 42 }; // OK

允许创建易于使用的强类型 ...

21. 更严的表达式求值次序

简而言之,对于诸如 f(a, b, c) 之类的表达式,标准并未规定子表达式 f、a、b、c(形状任意)的求值顺序。

cpp 复制代码
// 以下代码无具体行为
f(i++, i);

v[i] = i++;

std::map<int, int> m;
m[0] = m.size(); // {{0, 0}} or {{0, 1}} ?

变更摘要:

后缀表达式从左到右求值,包括函数调用和成员选择表达式。

赋值表达式从右到左求值,包括复合赋值。

移位运算符的操作数从左到右求值。

22. constexpr lambda 表达式

constexpr可以用于 lambda 表达式的上下文中。例如:

cpp 复制代码
constexpr auto ID = [] (int n)  { return n; };
constexpr int I = ID(3);
static_assert(I == 3);

constexpr int AddEleven(int n) {
  //由于"n"是字面类型,因此可以在常量表达式中初始化"数据成员"n。
  return [n] { return n + 11; }();
}
static_assert(AddEleven(5) == 16);

23. 范围 for 中不同的 begin 和 end 类型

cpp 复制代码
{
  auto && __range = for-range-initializer;
  auto __begin = begin-expr;
  auto __end = end-expr;
  for ( ; __begin != __end; ++__begin ) {
    for-range-declaration = *__begin;
    statement
  }
}

__begin 和 __end 的类型可能不同;只需要比较运算符即可。这一小小的改动能为 Range TS 用户带来更好的体验。

24. 新增属性 [[fallthrough]]

表示 switch 语句中的贯穿(fallthrough)(即语句后不加 break)操作是故意的,不应为此发出警告。例如:

cpp 复制代码
switch (c) {
case 'a':
    f(); // 提示警告, 也许贯穿是程序员犯错
case 'b':
    g();
[[fallthrough]]; // 抑制警告, 贯穿是有意为之
case 'c':
    h();
}

25. 新增属性 [[nodiscard]]

\[nodiscard\]\] 用于强调函数的返回值不能被丢弃,否则会收到编译器警告。例如: ```cpp [[nodiscard]] int foo(); void bar() { foo(); // 警告,不可丢弃返回值 } ``` 此属性也可应用于类型,以便将所有返回该类型的函数标记为 \[\[nodiscard\]\]: ```cpp [[nodiscard]] struct DoNotThrowMeAway{}; DoNotThrowMeAway i_promise(); void oops() { i_promise(); // 警告提示, 返回值不可丢弃 } ``` ## 26. 新增属性 `[[maybe_unused]]` 当未使用的实体用 \[\[maybe_unused\]\] 声明时,抑制有关未使用实体的编译器警告。 ```cpp static void impl1() { ... } // 编译器可能会产生警告 [[maybe_unused]] static void impl2() { ... } // 编译器不会产生警告(若未使用) void foo() { int x = 42; // 编译器可能会产生警告 [[maybe_unused]] int y = 42; // 编译器不会产生警告 } ``` ## 27. 忽略未知属性而不报错 明确规定实现应忽略任何不支持的属性命名空间,这在C++2017之前会提示这是未指定的。 ```cpp //不支持 MyCompilerSpecificNamespace 的编译器将忽略此属性 [[MyCompilerSpecificNamespace::do_special_thing]] void foo(); ``` ## 28. 在 using 声明中使用打包扩展 允许你从参数包中的所有类型注入带有 using 声明的名称。 为了在可变参数模板中从所有基类公开 operator(),我们过去不得不求助于递归: ```cpp template struct Overloader : T, Overloader { using T::operator(); using Overloader::operator(); // [...] }; template struct Overloader : T { using T::operator(); }; ``` 现在我们可以直接在 using 声明中展开参数包: ```cpp template struct Overloader : Ts... { using Ts::operator()...; // [...] }; ``` ## 29. 新增结构化绑定声明 当使用元组作为返回类型时,此功能非常有用。它会自动创建变量并将它们关联起来。 "分解声明"这个名称也曾使用,但最终标准同意使用"结构化绑定声明"。 例如: ```cpp // 必须先声明 a, b, c int a = 0; double b = 0.0; long c = 0; std::tie(a, b, c) = tuple; ``` 现在我们可以写成: ```cpp auto [ a, b, c ] = tuple; ``` ## 30. 新增十六进制浮点文字量(字面量) 允许表示一些特殊的浮点值,例如,最小的普通 IEEE-754 单精度值可以很容易地写成 0x1.0p-126 。 ```cpp double f = 0x1.0p-126; ``` ## 31. if 和 switch 语句的初始化语句 C++ 中 if 和 switch 语句的新版本: `if (init; condition)` 和 `switch (init; condition)` `这样可以简化代码。例如,以前你需要写:` ```cpp { auto val = GetValue(); if (condition(val)) // on success else // on false... } ``` 显然,这个值有单独的作用域,没有它就会"泄漏"。 而现在可以写成: ```cpp if (auto val = GetValue(); condition(val)) // on success else // on false... ``` val 仅在 if 和 else 语句内部可见,因此不会"泄露"。condition 可以是任何条件,而不仅仅是 val 为真/假。 ## 32. 允许内联变量 以前只能将方法/函数指定为内联代码,现在也可以在头文件中对变量执行相同的操作。 > 内联声明的变量与内联声明的函数具有相同的语义:它可以在多个翻译单元中以相同的方式定义,必须在使用它的每个编译单元中定义,并且程序的行为就像只有一个变量一样。 ```cpp struct MyClass { static const int sValue; }; inline int const MyClass::sValue = 777; ``` 甚至可以写成: ```cpp struct MyClass { inline static const int sValue = 777; }; ``` ## 33. 模板参数匹配排除兼容模板 > 本文允许模板参数绑定到模板实参,前提是该模板参数至少与模板实参一样具有特殊性。这意味着任何可以合法应用于模板参数的模板实参列表也适用于实参模板。 例如: ```cpp template