C++17新语法及标准库


1. New language features

1.1. New auto rules for direct-list-initialization

在C++ 17中,引入了一个新的auto规则来改进直接列表初始化(direct-list-initialization)的行为。在此前,使用auto声明变量并对其进行直接列表初始化时,结果类型通常为std::initializer_list。这意味着该变量只能表示一个具有可复制语义的对象集合,并且很难用于处理其他类型的值。
在C++ 17中,当使用auto声明变量并对其进行直接列表初始化时,会根据以下规则推断出结果类型:

  • 如果初始化列表仅包含单一元素,则将该元素的类型作为结果类型;
  • 否则,如果初始化列表的所有元素都可以转换为某个目标类型T,则将T作为结果类型;
  • 否则,将初始化列表的类型作为结果类型。

下面是一些示例代码,演示了如何使用新的auto规则进行直接列表初始化:

// Example 1: Single-element list
auto x1{42};     // Type of x1 is int

// Example 2: Homogeneous list
auto v1{1, 2, 3};   // Type of v1 is std::initializer_list<int>
auto v2{1.0, 2.0, 3.0};   // Type of v2 is std::initializer_list<double>

// Example 3: Heterogeneous list
struct Point { int x; double y; };
auto p{Point{1, 2.0}, Point{3, 4.0}};   // Type of p is std::initializer_list<Point>

在上面的示例中,我们使用了新的auto规则来声明变量并对其进行直接列表初始化。在第一个示例中,由于列表只包含单个元素,因此结果类型为int。在第二个示例中,由于列表中的所有元素都具有相同的类型(int或double),因此结果类型为std::initializer_list或std::initializer_list。在第三个示例中,由于列表中的元素具有不同的类型(Point类型的对象),因此结果类型为std::initializer_list。
需要注意的是,虽然这种新的auto规则可以改进直接列表初始化的行为,但也可能导致一些类型推断问题。因此,在实际应用中,应当谨慎使用,并且仔细检查变量的类型是否符合预期。

1.2. static_assert with no message

在C++17中,可以使用static_assert语句来在编译时进行条件检查,并在条件不满足时抛出一个静态断言错误。如果要使用static_assert语句而不指定任何错误消息,只需省略第二个参数即可。
下面是一个示例代码,演示了如何使用不带错误消息的static_assert语句:

template<typename T>
void foo(T x) {
    static_assert(std::is_integral_v<T>, "T must be an integral type");  // C++11 and later support 静态断言:T必须为整数类型
   	static_assert(std::is_integral_v<T>); // C++17 Support
    // ...
}

在上面的示例中,我们定义了一个函数foo,并使用static_assert语句来检查模板参数T是否为整数类型。由于没有指定错误消息,编译器会默认输出一个通用错误消息。
需要注意的是,尽管不指定错误消息可能会使得错误信息不够明确,但在某些情况下,这种简洁的写法可能更加清晰和易读。因此,在使用static_assert语句时,应当根据具体情况选择合适的错误消息。

1.3. typename in a template template parameter

在C++17中,关键词typename可以在模板模板参数中作为class的同义词使用。例如:

template <template <typename> typename Container>
void foo(Container<int> c) {
    // ...
}

这个函数模板定义了一个模板模板参数Container,它本身需要一个类型参数(用typename表示)。该函数然后使用int实例化Container,以创建类型为Container<int>的对象。

1.4. Removing [trigraphs]

在C++17中,三字符组已被移除,因为它们被认为是一种遗留功能,容易造成混淆,而且很少被使用。三字符组是由三个字符组成的序列,代表源代码中的单个字符。例如,“??” 可以代替 “|” 运算符。移除三字符组简化了语言,并减少了代码解释时出错的可能性。

1.5. Nested namespace definition

在 C++17 中,可以使用嵌套命名空间定义(Nested namespace definition),允许将一个命名空间嵌套到另一个命名空间中。这个特性有助于组织和管理代码库,使得命名空间更加清晰易读。
例如,可以通过以下方式定义一个嵌套命名空间:

namespace foo::bar {
// code here
}

在上面的例子中,命名空间 bar 是嵌套在命名空间 foo 中的。我们也可以进一步嵌套命名空间:

namespace foo::bar::baz {
// code here
}

在这种情况下,命名空间 baz 是嵌套在命名空间 bar 中的。通过使用嵌套命名空间定义,我们可以更清晰地表达代码层次结构,提高代码的可读性和可维护性。

1.6. Attributes for namespaces and enumerators

在C++17中,引入了对命名空间和枚举类型成员的属性(Attributes)支持。这个特性允许我们给命名空间或枚举类型成员添加各种元数据信息,例如编译器指令、优化选项等。
以下是一个示例,演示如何使用属性为命名空间添加元数据:

[[nodiscard]] namespace foo {
// code here
}

在上面的例子中,我们在 foo 命名空间前加上了 nodiscard 属性,表示该命名空间返回的结果应该被检查并处理。
类似地,我们可以为枚举类型成员添加属性,例如:

enum class Color {
Red,
Green,
[[deprecated("Use Blue_v instead")]] Blue
    };

在上面的例子中,我们为 Blue 枚举值添加了 deprecated 属性,并指定了一条消息,表示该枚举值已过时。这样,在使用 Blue 枚举值时,编译器会发出警告。
通过使用属性,我们可以向代码添加更多的元数据信息,帮助编译器和其他开发人员更好地理解代码的含义和用途。

1.7. u8 character literals

在 C++17 中,添加了对 UTF-8 字符字面量char8_t(u8 character literals)的支持。UTF-8 是一种可变长编码,可以用来表示 Unicode 字符集中的任意字符
使用 u8 前缀可以指示编译器将后面的字符序列解释为 UTF-8 格式。以下是一个示例:

const char8_t* str = u8"你好,世界!";

这种方式可以强制指定存储内存的格式。

1.8. Allow constant evaluation for all non-type template arguments

在 C++17 中,允许对所有非类型模板参数(Non-type template argument)进行常量表达式求值(Constant expression evaluation)。这意味着我们可以在编译时计算所有的非类型模板参数,并作为模板实例化的一部分。
例如,以下是一个使用非类型模板参数的示例:

template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N-1>::value;
};

template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

int main() {
    constexpr int fact5 = Factorial<5>::value;  // 求5的阶乘
    return 0;
}

在上面的代码中,我们定义了一个 Factorial 模板类来计算给定整数的阶乘。该模板类接受一个非类型模板参数 N,并通过递归方式计算阶乘。我们在 main() 函数中使用该模板类来计算 5 的阶乘,并将结果存储在常量变量 fact5 中。
在 C++17 中,由于对所有非类型模板参数进行了常量表达式求值的支持,编译器可以在编译时计算 Factorial<5>::value 的值,并将其视为常量表达式。这使得我们可以在程序中使用 constexpr 关键字来声明和使用常量变量 fact5,从而提高了程序的性能和可读性。
总之,在 C++17 中,我们可以放心地使用所有非类型模板参数,并将其作为常量表达式来求值。

1.9. Fold Expressions

在 C++17 中,引入了折叠表达式(Fold expressions)这一新特性。折叠表达式允许我们使用可变参数模板和运算符来简化对参数包中所有元素的操作。
以下是一个示例,演示如何使用折叠表达式计算参数包中所有元素的总和:

template<typename... Args>
auto sum(Args... args) {
    return (args + ...);
}

int main() {
    int result = sum(1, 2, 3, 4, 5);
    return 0;
}

在上面的代码中,sum 函数接受若干个参数,并使用折叠表达式 (args + ...) 将它们相加。这个折叠表达式展开后的效果等价于 (1 + 2 + 3 + 4 + 5),因此该函数返回 15
除了求和,我们还可以使用折叠表达式进行其他类型的操作,例如字符串连接、逻辑或、位运算等等。折叠表达式使得对参数包中所有元素的操作变得更加简洁易懂,提高了程序的可读性和可维护性。
需要注意的是,折叠表达式只支持某些特定类型的运算符,例如二元运算符、逗号运算符等。如果使用不支持的运算符或表达式,则会导致编译错误。

1.10. Unary fold expressions and empty parameter packs

在 C++17 中,引入了一些增强型的折叠表达式(Fold expressions)特性,包括一元折叠表达式(Unary fold expressions)和对空参数包的支持。
一元折叠表达式允许我们使用一元运算符对参数包中所有元素进行操作。以下是一个示例:

template<typename... Args>
bool all(Args... args) {
    return (... && args);
}

int main() {
    bool b1 = all(true, true, true);  // 返回 true
    bool b2 = all(true, false, true); // 返回 false
    return 0;
}

在上面的代码中,all 函数接受若干个参数,并使用一元折叠表达式 (... && args) 将它们进行逻辑与操作。这个一元折叠表达式展开后的效果等价于 (true && true && true)(true && false && true),因此该函数分别返回 truefalse
对于空参数包,C++17 中也提供了特殊处理。这意味着我们可以在模板中使用空参数包而无需再编写特殊情况的代码。以下是一个示例:

template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args); // 折叠表达式展开
}

int main() {
    print("Hello", ",", " ", "world", "!");
    return 0;
}

在上面的代码中,print 函数接受若干个参数,并使用折叠表达式 (std::cout << ... << args) 将它们依次输出到标准输出流中。由于该函数支持空参数包,因此我们可以不传递任何参数调用该函数。
通过使用一元折叠表达式和对空参数包的支持,我们可以更加方便地处理模板编程中的各种情况,提高程序的可读性和可维护性。

1.11. Remove Deprecated Use of the register Keyword

在 C++17 中,已经删除了对 register 关键字的推荐使用。在早期版本的 C++ 中,register 关键字被用于建议编译器将变量存储在 CPU 寄存器中,以便提高程序的性能。但是,随着编译器和计算机硬件的发展,现代编译器已经可以自动地优化变量的存储方式,因此 register 关键字已经成为过时的特性。
在 C++17 中,如果我们仍然使用 register 关键字来建议编译器将变量存储在寄存器中,则会被视为编译错误。

1.12. Remove Deprecated operator++(bool)

在 C17 中,已经删除了对后置递增运算符 operator++(int) 的推荐使用。在早期的 C 版本中,我们可以通过后置递增运算符来实现迭代器的遍历等操作。而在之前的版本中,如果我们想要重载后置递增运算符,需要提供一个带有一个 int 参数的函数 operator++(int)
然而,在 C17 中,该函数已被标记为过时的特性,并在未来的 C 版本中可能会被完全删除。取而代之的是,我们应该使用不带参数的后置递增运算符 operator++(),并返回一个无用的值以满足语言规范的要求。例如:

class Iterator {
public:
    // 不带参数的后置递增运算符
    Iterator operator++(int) {
        Iterator tmp = *this;
        ++*this;
        return tmp;
    }
    
    // 前置递增运算符
    Iterator& operator++() {
        // ...
        return *this;
    }
};

在上面的代码中,我们通过重载后置递增运算符 operator++(int) 来实现迭代器的遍历。在 C++17 中,我们应该改为使用不带参数的后置递增运算符 operator++(),并在函数体中返回一个无用的值(即某个对象的副本)以满足语言规范的要求。同时,我们还需要修改前置递增运算符 operator++() 的实现,以满足新的语法规则。
总之,在 C++17 中,我们应该避免使用带有一个 int 参数的后置递增运算符 operator++(int),并改为使用不带参数的后置递增运算符 operator++(),以遵循最新的语言规范和编程标准。

1.13. Make exception specifications part of the type system

在 C++17 中,异常规格(Exception specification)已经成为了函数类型的一部分。这意味着,函数的异常规格对其类型有直接影响,并且可以更加精确地表达函数的行为。
在之前的 C++ 版本中,我们可以使用 throw()noexcept 关键字来指定函数的异常规格,但是这些关键字只是作为函数签名的一部分来进行处理,并不能完全捕获函数可能抛出的所有异常类型。在 C++17 中,我们可以使用新的语法来定义函数的异常规格,并将其作为函数类型的一部分来进行考虑。
以下是一个示例,演示如何使用新的语法来定义函数的异常规格:

void foo() noexcept;
void bar() throw(int, std::runtime_error);

int main() {
    using FooType = decltype(foo); // 类型为 void (*)()
    using BarType = decltype(bar); // 类型为 void (*)() throw(int, std::runtime_error)
    return 0;
}

在上面的代码中,我们定义了两个函数 foobar,分别使用不同的语法来指定它们的异常规格。在 main() 函数中,我们使用 decltype 关键字和函数指针类型来检查函数的类型,并观察其是否包含了异常规格信息。
需要注意的是,即使在 C++17 中,函数的异常规格依然是可选的,并且并没有强制要求所有函数都必须具有异常规格。但是,通过将异常规格作为函数类型的一部分来进行考虑,我们可以更加精确地描述函数的行为,并在编译时对其进行静态检查,从而提高程序的稳定性和可靠性。
总之,在 C++17 中,我们应该尽可能地使用新的语法来定义函数的异常规格,并将其作为函数类型的一部分来进行考虑,以提高程序的稳定性和可维护性。

1.14. [Aggregate classes]

在 C17 中,聚合类(Aggregate class)可以包含基类(Base class),从而更加灵活地组织类的层次结构和数据成员。聚合类是指仅包含公共非静态数据成员的类,在之前的 C 版本中,聚合类不能包含任何基类。但是,在 C++17 中,聚合类可以包含具有默认构造函数、复制构造函数和析构函数的基类。
以下是一个示例,演示如何定义一个聚合类并包含一个具有构造函数和析构函数的基类:

struct Base {
int x;
Base(int n) : x(n) {}
virtual ~Base() {}
};

struct Aggregate : Base {
int y;
};

int main() {
    Aggregate aggr = {10, 20};
    return 0;
}

在上面的代码中,我们定义了两个类 BaseAggregate,其中 Aggregate 继承自 Base 并包含一个额外的数据成员 y。在 main() 函数中,我们使用聚合初始化语法来初始化一个 Aggregate 对象,并传递了两个参数 1020 分别给 BaseAggregate 的数据成员。
需要注意的是,为了让 Aggregate 成为一个聚合类,我们需要保证它只包含公共非静态数据成员,并且没有用户声明的构造函数、析构函数和虚函数等。同时,基类的构造函数和析构函数需要符合一些限制,例如不能是虚函数或纯虚函数等。
总之,在 C++17 中,我们可以使用聚合类来更加灵活地组织类的层次结构和数据成员,并且聚合类可以包含具有默认构造函数、复制构造函数和析构函数的基类。

1.15. __has_include in preprocessor conditionals

在 C++17 中,引入了一个新的预处理宏 __has_include,可以用于判断某个头文件是否可用。该宏返回值为 1 表示该头文件可用,否则表示不可用。
使用 __has_include 可以避免在编译时出现找不到头文件的错误,并且可以根据头文件的可用性来进行条件编译。例如:

#if __has_include(<optional>)
#include <optional>
using std::optional;
#else
#error "This program requires <optional>"
#endif

int main() {
    optional<int> i = 42;
    return 0;
}

在上面的代码中,我们使用 __has_include 和条件编译来检查 <optional> 头文件是否可用。如果可用,则包含该头文件并使用其中的 std::optional 类;否则抛出编译错误并提示用户需要使用该头文件才能编译程序。
需要注意的是,__has_include 只能用于检测头文件是否存在,但不能判断头文件的版本或功能是否符合要求。因此,在使用该宏时,我们需要仍然需要对头文件进行进一步的检查和测试,以确保其功能符合要求。
总之,在 C++17 中,我们可以使用 __has_include 宏来判断某个头文件是否可用,并根据其结果进行条件编译和错误提示等处理。这有助于提高程序的可移植性和可靠性。

1.16. New specification for inheriting constructors

在 C++17 中,引入了一种新的继承构造函数(Inheriting constructors)的规范方式,可以更加方便地重用基类的构造函数。该规范方式使用关键字 using,并通过指定基类的构造函数来实现继承。
以下是一个示例,演示如何使用新的规范方式来继承基类的构造函数:

class Base {
public:
Base(int n) {}
};

class Derived : public Base {
public:
using Base::Base; // 继承 Base 的构造函数
};

在上面的代码中,我们定义了两个类 BaseDerived,其中 Derived 继承自 Base 并使用关键字 using 来继承其构造函数。这样,在创建 Derived 的对象时,我们可以直接使用 Base 的构造函数来进行初始化。
需要注意的是,为了使用继承构造函数规范方式,我们需要保证基类的构造函数在派生类中是可见的,并且没有被重载或隐藏等。同时,我们还可以通过默认参数、委托构造函数等方式来修改或重载基类的构造函数,并将其应用于派生类的对象初始化。
总之,在 C++17 中,我们可以使用新的继承构造函数规范方式来更加方便地重用基类的构造函数,并提高类的可读性和可维护性。

1.17. Lambda capture of *this

在 C++17 中,可以使用新的语法来捕获类的成员变量和函数,包括通过 *this 指针捕获整个类对象或指向类对象的指针。这种方法被称为 lambda 捕获(Lambda capture),可以使 lambda 表达式更加灵活地访问外部作用域中的变量和函数。
以下是一个示例,演示如何使用 *this 指针来捕获整个类对象:

class Example {
public:
void foo() {
    auto lambda = [*this]() {
        this->bar();
        x = 42;
    };
    lambda();
}

void bar() {}

private:
int x;
};

int main() {
    Example example;
    example.foo();
    return 0;
}

在上面的代码中,我们定义了一个类 Example,其中的 foo() 成员函数创建了一个 lambda 表达式,并通过 *this 指针来捕获整个类对象。在 lambda 表达式中,我们可以通过 this->bar() 访问类的成员函数 bar(),并通过 x = 42 修改类的私有变量 x。最后,我们调用 lambda 表达式以执行相应的操作。
需要注意的是,lambda 捕获会将类对象或指针复制到 lambda 表达式中,并将其作为闭包对象的一部分进行存储。因此,在使用 *this 指针捕获类对象时,我们需要注意复制和移动语义等问题,以避免对原始类对象造成影响。
总之,在 C++17 中,可以使用新的语法来捕获类的成员变量和函数,并通过 *this 指针来捕获整个类对象或指向类对象的指针。这有助于使 lambda 表达式更加灵活地访问外部作用域中的变量和函数。

1.18. Direct-list-initialization of enumerations

在 C11 中引入了一种新的初始化方式——直接列表初始化(Direct-list-initialization),可以用于各种类型的对象,包括枚举类型。在 C17 中,对枚举类型的直接列表初始化进行了扩展,使得该初始化方式更加灵活和易用。
以下是一个示例,演示如何使用直接列表初始化来初始化枚举类型:

enum class Color {
RED,
GREEN,
BLUE
};

int main() {
    // 直接列表初始化
    Color c1{Color::RED};

    // 旧的复合字面量语法
    Color c2 = {Color::GREEN};

    // 使用整数值初始化
    Color c3{static_cast<Color>(2)};

    return 0;
}

在上面的代码中,我们定义了一个枚举类型 Color,并使用不同的方式进行直接列表初始化。例如,通过 {Color::RED}{Color::GREEN} 来使用枚举常量进行初始化;通过 {static_cast<Color>(2)} 来使用整数值进行初始化。需要注意的是,由于枚举类型是强类型枚举(Strongly-typed enumeration),因此必须显式转换为正确的枚举类型。
需要注意的是,在 C++17 中,使用花括号初始化的方式也可以用于对其他类型的对象进行初始化,例如数组、结构体、类等。这种初始化方式具有更好的类型检查和安全性,并且可以避免一些隐式类型转换带来的问题。
总之,在 C++17 中,对枚举类型的直接列表初始化进行了扩展,使得该初始化方式更加灵活和易用,并且可以应用于各种类型的对象。

1.19. constexpr lambda expressions

在 C++17 中,引入了对 lambda 表达式的 constexpr 支持,可以使 lambda 表达式成为常量表达式。这意味着 lambda 表达式可以用于编译时计算,并且可以在编译时进行优化和验证。
以下是一个示例,演示如何使用 constexpr 修饰 lambda 表达式:

constexpr auto fib = [](int n) {
    if (n <= 1)
        return n;
    else
        return fib(n - 1) + fib(n - 2);
};

int main() {
    constexpr int result = fib(6); // 在编译时计算斐波那契数列第6项
    static_assert(result == 8, "Wrong calculation");
    return 0;
}

在上面的代码中,我们定义了一个 lambda 表达式 fib,并使用 constexpr 修饰其类型以使其成为常量表达式。然后,在 main() 函数中,我们使用 fib(6) 来计算斐波那契数列的第6项,并将结果赋值给 result 变量。最后,我们使用 static_assert 来检查结果是否正确。
需要注意的是,使用 constexpr 修饰 lambda 表达式有一些限制,例如不能捕获任何变量、不能包含控制流程语句(例如循环和分支)、不能抛出异常等。同时,lambda 表达式的参数和返回值类型必须是字面类型(Literal type),以便在编译时进行计算和优化。
总之,在 C++17 中,对 lambda 表达式的 constexpr 支持使其成为常量表达式,并可以在编译时进行计算和优化。这有助于提高程序的性能和可靠性。

1.20. Differing begin and end types in range-based for

在 C11 中引入了范围-based for 循环(range-based for loop),可以用于遍历各种容器类型,例如数组、向量、列表等。在 C17 中,扩展了范围-based for 循环的支持,使得循环中的 begin 和 end 函数返回不同的类型。
以下是一个示例,演示如何使用不同的 begin 和 end 函数来遍历数组中的奇数和偶数元素:

#include <array>
#include <iostream>

struct OddElements {
int* p;

int* begin() const { return p; }

int* end() const { return p + 10; }
};

struct EvenElements {
int* p;

int* begin() const { return p + 1; }

int* end() const { return p + 10; }
};

int main() {
    std::array<int, 10> arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    OddElements odds{arr.data()};
    for (auto i : odds) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    EvenElements evens{arr.data()};
    for (auto i : evens) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上面的代码中,我们定义了两个结构体 OddElementsEvenElements,分别表示数组中的奇数和偶数元素。这两个结构体中,begin() 函数和 end() 函数的返回类型不同。然后,在 main() 函数中,我们分别使用这两个结构体来遍历数组中的奇数和偶数元素。
需要注意的是,虽然 C++17 扩展了范围-based for 循环的支持,但并不是所有容器类型都支持不同的 begin 和 end 函数。因此,在使用这种特性时,需要查阅相应的文档以确定容器类型是否支持。
总之,在 C++17 中,扩展了范围-based for 循环的支持,使得循环中的 begin 和 end 函数可以返回不同的类型。这有助于更加灵活地遍历各种容器类型,并且提高了可读性和可维护性。

1.21. [[fallthrough]] attributestd::uncaught_exceptions()

是一个 C++17 引入的标准库函数,用于返回当前捕获但未处理的异常数。该函数返回一个整数值,表示当前活动的异常处理器中还有多少个异常没有被处理。

该函数与 std::uncaught_exception() 不同,后者用于检查是否已经有异常被抛出但尚未处理。而 std::uncaught_exceptions() 则可以返回未处理的所有异常数,这对于一些需要在程序中处理多个异常的情况非常有用。

例如,以下代码演示了如何使用 std::uncaught_exceptions() 来处理多个异常:

#include <iostream>
#include <exception>

int main() {
    int count = 0;
    try {
        try {
            throw std::exception();
        } catch (...) {
            ++count;
            throw;
        }
    } catch (...) {
        ++count;
    }

    std::cout << "Number of uncaught exceptions: " << std::uncaught_exceptions() << '\n';
    std::cout << "Total number of exceptions thrown: " << count << '\n';

    return 0;
}

在上面的示例中,我们嵌套了两个 try-catch 块,并在内层的 catch 块中重新抛出异常。当程序运行时,将会抛出两个异常,并在外层的 catch 块中捕获它们。此时,std::uncaught_exceptions() 应该返回 2,表示还有两个未处理的异常。
C++17 引入了 [[fallthrough]] 属性,它是一个用于在 switch 语句中标注“落入”行为的特殊属性。当某个 case 语句没有使用 break 或 return 等跳出语句时,程序将会执行下一条 case 语句,这种情况称为“落入”(fallthrough)。使用 [[fallthrough]] 属性可以显式地指示这种行为,使得代码更加清晰易懂。
以下是一个示例,演示如何使用 [[fallthrough]] 属性:

#include <iostream>

int main() {
    int x = 3;
    switch (x) {
        case 1:
            std::cout << "Case 1\n";
            break;
        case 2:
            [[fallthrough]];
        case 3:
            std::cout << "Case 2/3\n";
            break;
        default:
            std::cout << "Default case\n";
            break;
    }
    return 0;
}

在上面的代码中,我们使用 switch 语句来匹配不同的值,并根据情况输出相应的结果。在 case 2 中,我们使用 [[fallthrough]] 属性来显式地指示程序将继续执行下一个 case 语句。
需要注意的是,[[fallthrough]] 属性只能用在 switch 语句中的 case 标签上,不能用在函数或其他语言结构中。同时,编译器可能会发出警告或错误信息,以防止潜在的错误使用。
总之,在 C++17 中,引入了 [[fallthrough]] 属性,用于在 switch 语句中标注“落入”行为。这有助于使代码更加清晰易懂,并且可以提高程序的可读性和可维护性。

1.22. [[maybe_unused]] attribute

C++17 引入了 [[maybe_unused]] 属性,它是一个用于标记未使用变量或实体的特殊属性。在编写代码时,有时会定义一些变量或函数,但由于某些原因可能没有使用它们,这可能导致编译器发出警告或错误。使用 [[maybe_unused]] 属性可以明确地告诉编译器这些实体不是必需的,并且可以避免不必要的警告或错误信息。
以下是一个示例,演示如何使用 [[maybe_unused]] 属性:

[[maybe_unused]] int f()        //没有被使用的函数
{
    return 1;
}


int main()
{
    [[maybe_unused]] int i = 0; //没有被使用的变量
    return 0;
}

在上面的代码中,我们定义了一个带有 [[maybe_unused]] 属性的函数 foo,并在 main 函数中定义了一个变量 y,但未使用它。在这种情况下,编译器不会发出任何警告或错误信息,因为我们已经使用了 [[maybe_unused]] 属性来明确表示这些实体并非必需的。
需要注意的是,[[maybe_unused]] 属性只能用于具有副作用(side effect)的实体,例如变量、函数等。对于没有副作用的实体,例如类型别名、枚举类型等,使用该属性是无效的。
总之,在 C++17 中,引入了 [[maybe_unused]] 属性,用于标记未使用变量或实体。这有助于避免不必要的警告或错误信息,并提高程序的可读性和可维护性。

1.23. Hexadecimal floating-point literals

在 C++17 中,引入了十六进制浮点字面量(hexadecimal floating-point literals)的支持。十六进制浮点字面量是一种用于表示精确浮点值的语法,可以方便地表示非常大或非常小的浮点数,并且避免了浮点数舍入误差带来的问题。
以下是一个示例,演示如何使用十六进制浮点字面量:

#include <iostream>
#include <iomanip>

int main() {
    double x = 0x1.ffffp3;  // 表示十六进制数 1.1111...(二进制)* 2^3
    double y = 0x1p-4;     // 表示十六进制数 1.0(二进制)* 2^-4

    std::cout << std::setprecision(16) << x << std::endl;
    std::cout << std::setprecision(16) << y << std::endl;

    return 0;
}

在上面的代码中,我们使用两个十六进制浮点字面量来初始化变量 x 和 y。变量 x 表示十六进制数 1.ffff(二进制)乘以 2 的 3 次方,即 15.984375。变量 y 表示十六进制数 1.0(二进制)乘以 2 的 -4 次方,即 0.0625。使用 std::setprecision 函数可以将输出的精度设置为 16 位小数,以便查看十六进制浮点字面量的精确值。
需要注意的是,十六进制浮点字面量的指数部分必须用 p 或 P 来表示,而不是 e 或 E。同时,C++17 中规定了十六进制浮点字面量的有效范围是 -1021 到 1024(包括这两个值),因此不能表示超出这个范围的浮点数。
总之,在 C++17 中,引入了十六进制浮点字面量的支持,使得我们可以方便地表示非常大或非常小的浮点数,并且避免了舍入误差带来的问题。

1.24. Using attribute namespaces without repetition

在C++17中,你可以使用using关键字来避免重复使用属性命名空间。例如,如果想要使用[[gnu::deprecated]]命名空间中的属性,可以这样写:

using namespace gnu::attributes;
[[deprecated]] void foo(); // 等价于 [[gnu::deprecated]] void foo();

这样可以避免每次使用时都需要重复命名空间的问题。但是,在使用命名空间时要小心,因为它可能会导致命名冲突和歧义。

1.25. Dynamic memory allocation for over-aligned data

在C++17中,可以使用newdelete关键字来动态分配超对齐(over-aligned)数据的内存。
通常情况下,分配内存时会遵守默认的对齐方式。例如,在x86架构上,默认的对齐方式是4字节。但是,有些类型需要更高的对齐方式,例如alignas(16) int x;指定了x需要按照16字节对齐。
为了支持超对齐数据的动态分配,C++17引入了两个新的类型:std::aligned_alloc()std::hardware_destructive_interference_size()
std::aligned_alloc()函数用于分配已知对齐方式的内存块。它的原型如下:

void* aligned_alloc(std::size_t alignment, std::size_t size);

其中,alignment是所需的对齐方式,以字节为单位;size是要分配的内存块大小,以字节为单位。该函数返回指向分配的内存块的指针,或者返回nullptr表示分配失败。
另外,std::hardware_destructive_interference_size()函数用于查询系统硬件级别的最大对齐方式。它的原型如下:

std::size_t hardware_destructive_interference_size();

该函数返回一个std::size_t类型的值,表示最大对齐方式,以字节为单位。
例如,下面的代码演示了如何使用std::aligned_alloc()函数来分配16字节对齐的内存块:

#include <iostream>
#include <cstdlib>

int main()
{
    std::size_t alignment = 16;
    std::size_t size = 32;
    void* ptr = std::aligned_alloc(alignment, size);
    if (ptr) {
        std::cout << "allocated " << size << " bytes at " << ptr << '\n';
        std::free(ptr); // remember to free the memory
    }
    else {
        std::cout << "allocation failed\n";
    }
}

需要注意的是,这些新函数可能并不被所有编译器支持。因此,在使用它们时应该检查编译器的支持情况。

1.26. Class template argument deduction

C++17中的类模板参数推导(Class Template Argument Deduction)是一项新特性,它可以在实例化类模板时省略模板参数,让编译器自动推导出模板参数的类型。这个特性使得使用类模板更加方便和简洁。
在C++17中,可以使用类模板的构造函数进行参数推导。例如,对于一个类模板MyClass,如果其构造函数的参数类型可以被推导出来,则可以省略模板参数,直接使用构造函数来实例化类模板:

MyClass obj(arg1, arg2, ...);

其中,arg1arg2等是构造函数的实参,编译器会根据这些实参推导出模板参数的类型。
需要注意的是,只有在以下情况下,编译器才能够推导出模板参数的类型:

  • 类模板的构造函数参数类型可以被推导出来。
  • 类模板的构造函数只有一个,或者是所有构造函数的参数类型都相同。

如果类模板的构造函数参数类型无法推导出来,或者存在多个构造函数且参数类型不同,则必须显式指定模板参数。

1.27. Non-type template parameters with auto type

C++17允许使用“auto”类型作为非类型模板参数的占位符,这在某些情况下可以简化代码并提高可读性。例如,如果我们要定义一个矩阵类,其行和列数都是整数常量表达式,可以这样写:

template <auto rows, auto cols>
class Matrix {
//...
};

在使用时,可以传递任何支持编译时求值的表达式作为实参,例如:

Matrix<3, 4> mat1;
const int n = 5;
Matrix<n, n+1> mat2;

需要注意的是,由于非类型模板参数必须在编译期间就能确定其值,因此使用“auto”类型的非类型模板参数时,编译器会在实例化时通过推导尝试将其转换为已知类型,否则会导致编译错误。

1.28. Guaranteed copy elision

在C++17中,对于一些情况下的对象构造和返回值传递,编译器被保证可以自动优化掉复制和移动操作,称为“Guaranteed copy elision”(担保的复制省略)。
具体来说,当以下三种情况之一发生时,编译器将会进行担保的复制省略:

  1. 在返回语句中从函数中返回一个临时变量;
  2. 在throw表达式中抛出一个临时变量;
  3. 在构造函数中使用初始化列表初始化一个非静态成员变量,初始化表达式是同一个类中的另一个对象或者具有相同类型和相同cv限定符的对象。

例如:

class MyClass {
public:
MyClass() {
    std::cout << "MyClass default ctor\n";
}
MyClass(const MyClass&) {
    std::cout << "MyClass copy ctor\n";
}
};

MyClass func() {
    return MyClass{};
}

int main() {
    auto obj = func();
}

在上述代码中,虽然func()函数返回一个临时对象,但是由于C++17中的担保的复制省略规则,编译器可以直接将该临时对象构造在obj变量上,而不需要进行复制或移动操作。因此,上述代码输出如下:

MyClass default ctor

1.29. Replacement of class objects containing reference members

在C17之前,包含引用成员的类对象是不可以被复制或移动的,因为引用不能被重新绑定。C17允许定义包含引用成员的类,并且提供了一种新的方式来进行复制和移动操作:使用替换类型(replaced type)。
具体来说,当类对象被复制或移动时,它的引用成员会被替换成相应的值类型对象。这个过程被称为“引用无损展开”(reference transparent),它保留了原始对象的状态,并产生一个可复制和可移动的对象。
例如:

#include <iostream>

class MyClass {
public:
int& ref;
MyClass(int& r) : ref(r) {}
};

int main() {
    int i = 42;
    MyClass obj1(i);
    MyClass obj2 = obj1; // 复制
    std::cout << obj1.ref << ' ' << obj2.ref << '\n';
    i = 13;
    std::cout << obj1.ref << ' ' << obj2.ref << '\n';
}

在上述代码中,MyClass包含一个整型引用成员ref。由于引用成员不能被复制或移动,因此在C17之前,obj1对象不能被复制或移动。但是,在C17中,当obj1对象被复制到obj2时,编译器会自动将obj2的引用成员ref替换为相应的值类型对象,这使得obj2成为一个可复制和可移动的对象。

1.30. Stricter expression evaluation order

C++17 中引入了更加严格的表达式求值顺序规则,即在一个表达式中,子表达式的求值顺序不再是未定义的,而是被规定为从左到右。
以下是一个示例,说明了 C++17 中新的表达式求值顺序规则:

#include <iostream>

int f1(int i) {
    std::cout << "f1\n";
    return 1+i;
}

int f2(int i) {
    std::cout << "f2\n";
    return 2 + i;
}

int f3(int i) {
    std::cout << "f3\n";
    return 3 + i;
}

int main() {
    int i = 0;
    int a = f1(i++) + f2(i++) * f3(i++); // 加号左边先计算 f1(),然后乘号左边先计算 f2(),再计算 f3()
    std::cout << "a = " << a << "\n";
    return 0;
}

输出结果为:

f1
f2
f3
a = 16

可以看出,根据 C++17 的新规则,在表达式 f1() + f2() * f3() 中,首先计算 f1(),然后计算 f2(),最后计算 f3(),因此输出的顺序为 f1f2f3。最终得到的结果为 7,符合预期。

1.31. [Structured Bindings]

C++17 中引入了结构化绑定(Structured Bindings)的特性,可以使得从一个聚合类型中提取多个成员变量更加方便。
以下是一个示例,说明了如何使用结构化绑定:

#include <iostream>
#include <tuple>

std::tuple<int, double> f() {
    return std::make_tuple(1, 2.5);
}

int main() {
    // 使用结构化绑定从 tuple 中提取两个元素,并分别赋值给变量 i 和 d
    auto [i, d] = f();

    std::cout << "i = " << i << "\n";
    std::cout << "d = " << d << "\n";

    return 0;
}

输出结果为:

i = 1
d = 2.5

可以看出,在 main() 函数中,通过调用 f() 返回一个包含两个元素的 tuple,然后使用结构化绑定将这两个元素分别赋值给变量 id。结构化绑定的语法形式为 auto [a, b, c, ...] = x;,其中 x 是一个聚合类型,abc 等则是需要提取的成员变量名。
结构化绑定可以用于各种聚合类型,包括数组、std::pairstd::tuple 等等。

1.32. Ignore unknown attributes

C++17 中引入了一种新的特性,可以让编译器忽略掉它不认识的属性(Attribute),从而避免由于未知属性导致的编译错误。
以下是一个示例,说明了如何使用 C++17 中的这个特性:

[[unknown_attribute]] int f() {
    return 0;
}

int main() {
    f();
    return 0;
}

在上面的代码中,我们在 f() 函数前加上了一个名为 unknown_attribute 的未知属性。由于编译器不认识这个属性,如果直接编译上述代码,会导致编译错误。但是,如果我们使用 C++17 中的新特性,在未知属性前加上 [[fallthrough]],编译器就会忽略这个未知属性,不再报错。
修改后的代码如下:

[[fallthrough, unknown_attribute]] int f() {
    return 0;
}

int main() {
    f();
    return 0;
}

在上面的代码中,我们在未知属性前加上了 [[fallthrough]],这意味着在遇到未知属性时,编译器将会忽略该属性,并继续向下执行。这样,即使存在未知属性,编译器也能够正常编译代码,不再报错。
需要注意的是,使用这个特性可能会影响程序的正确性和可移植性,因此应该谨慎使用。

1.33. constexpr if statements

C++17 中引入了 constexpr if 语句,它可以在编译时根据条件选择性地执行代码块。
以下是一个示例,说明了如何使用 constexpr if

#include <iostream>

template<typename T>
void f(T t) {
    if constexpr (std::is_integral_v<T>) {  // 如果 T 是整数类型
        std::cout << "T is an integral type.\n";
        std::cout << "t + 1 = " << t + 1 << "\n";  // 执行这行代码
    }
    else {
        std::cout << "T is not an integral type.\n";
    }
}

int main() {
    f(3);
    f("hello");
    return 0;
}

输出结果为:

T is an integral type.
t + 1 = 4
T is not an integral type.

可以看出,f() 函数中使用了 constexpr if 语句,在模板参数 T 是整数类型时,会执行 if 语句块内的代码,否则执行 else 语句块内的代码。在上面的示例中,第一次调用 f(3) 时,T 被推导为 int,因此执行 if 语句块内的代码;第二次调用 f("hello") 时,T 被推导为 const char*,不是整数类型,因此执行 else 语句块内的代码。
需要注意的是,constexpr if 中的条件表达式必须在编译时求值,且结果必须是布尔类型。如果条件表达式的结果是 true,则编译器会执行其中的代码块;否则,该代码块中的代码不会被编译到最终的可执行文件中。这可以使得程序具有更好的性能和更小的体积。

1.34. init-statements for if and switch

C++17中引入了用于if和switch语句的初始化语句,可以在这些语句中定义并初始化变量。这意味着您可以在条件或开关表达式中定义变量并立即将其初始化。这为代码编写提供了更大的灵活性和可读性。
例如,使用初始化语句的if语句可能如下所示:

if (int x = compute_value(); x > 10) {
    // 这里使用x
}

在上面的代码中,我们定义了一个名为x的整数,并将其初始化为compute_value()函数的返回值。然后,我们检查是否满足某个条件,如果是,则在if语句块内使用x。
同样,使用初始化语句的switch语句可能如下所示:

switch (auto result = compute_value(); result) {
    case 0:
        // 处理result等于0的情况
        break;
    case 1:
        // 处理result等于1的情况
        break;
    default:
        // 处理其他情况
        break;
}

在上面的代码中,我们定义了一个名为result的自动变量,并将其初始化为compute_value()函数的返回值。然后,我们使用switch语句根据result的值执行不同的操作。

1.35. inline variables

C++17引入了内联变量(inline variables)的特性,它允许将变量声明为内联的,从而允许在头文件中定义变量而不会出现重复定义的错误。这个特性可以提高程序的性能并减少二进制文件大小。
以下是一个使用内联变量的简单示例:

// 头文件 example.h

inline int count = 0;
// 源文件 example.cpp

#include "example.h"
#include <iostream>

int main() {
    std::cout << "Count: " << count << std::endl;
    ++count;
    std::cout << "Count: " << count << std::endl;
    return 0;
}

在上述代码中,变量 count 被声明为内联变量。因此,它可以被定义在头文件里而不会出现重复定义的错误。同时,在 main() 函数中,我们可以直接使用 count 变量,而不需要额外的定义或声明。

1.36. Removing dynamic exception specifications

动态异常规格是在函数声明中使用 throw(type) 形式指定函数可能抛出的异常类型。例如:

void foo() throw(std::exception);

在C++17中,动态异常规格被完全删除,所有函数都可以通过 noexcept 关键字来指定其是否可能抛出异常。

1.37. Pack expansions in using-declarations

C++17 中引入了模板参数包扩展(Pack expansions)在 using 声明(using-declarations)中的使用。这使得我们可以更容易地将多个基类或多个命名空间的成员引入到当前作用域。
考虑以下示例:

template<typename... T> struct Base {};

template<typename... T>
struct Derived : Base<T...> {};

template<typename... T>
struct AnotherDerived : Base<T...> {};

template<typename... T>
void func(T... args) {}

int main() {
    using namespace std;
    using BaseTypes = Base<int, double, char>;
    using namespace DerivedTypes = Derived<int, double>;

    // 使用模板参数包扩展在 using 声明中引入多个成员
    using BaseTypes::operator=...;
    using DerivedTypes::operator=...;

    // 使用模板参数包扩展在函数调用中传递多个参数
    func(1, 2, 3)...;

    return 0;
}

在上述代码中,我们首先定义了两个模板结构体 DerivedAnotherDerived,它们都继承自 Base。然后,我们定义了一个函数模板 func,它接受任意数量的参数。在 main() 函数中,我们使用 using 声明将 std 命名空间和 BaseDerived 的模板实例导入当前作用域。接着,我们使用模板参数包扩展在 using 声明中引入多个成员,即将 BaseDerived 中所有的 operator= 都引入到当前作用域。最后,我们使用模板参数包扩展在函数调用中传递多个参数,即对 func 分别传递参数 1、2、3。

1.38. DR: Matching of template template-arguments excludes compatible templates

DR(Defect Report)是指 C++ 标准中的缺陷报告,其中 DR:Matching of template template-arguments excludes compatible templates 意为“模板模板参数匹配排除了兼容的模板”。
这个 DR 引入了一个新规则,即对于给定的模板模板实参,如果有多个模板与其匹配,则只选择最特化的那个模板。这个规则保证了编译器可以在多个可行的选项中进行选择时,总是选择最合适的那个。
考虑以下示例:

template <typename T> struct A {};
template <typename T> struct A<T*> {};

template <template<typename> class X> struct B {};

int main() {
    B<A> b; // 错误:匹配到了两个模板
    return 0;
}

在上述代码中,我们定义了两个模板 A,分别接受类型和指针类型作为模板实参。然后,我们定义了一个模板 B,它接受一个模板模板参数 X。在 main() 函数中,我们试图将 A 作为 B 的模板实参,但由于存在两个 A 模板与之匹配,编译器无法确定应该选择哪一个。
在旧的规则下,编译器会选择所有与模板模板实参匹配的模板,因此 B<A> 将被匹配到两个模板 A,导致编译错误。而在新的规则下,编译器将只选择最特化的那个模板 A<T*>,这样,B<A> 将会成功实例化为一个模板 B<A<T*>>

2. Library features

2.1. std::void_t

std::void_t 是一个 C++17 中的工具类型,用于帮助实现 SFINAE 技术(Substitution Failure Is Not An Error)。在模板元编程中,我们经常需要检查某个类型是否具有某个成员函数或成员类型等特定属性,如果没有,则排除该模板函数或类模板的候选项。这就是 SFINAE 技术的应用场景。
std::void_t 可以将任何一组类型都转化为 void 类型并返回,因此可以用于判断某些类型是否存在。例如,下面的代码使用 void_t 判断类型 T 是否具有成员类型 value_type

template <typename T, typename = void>
struct has_value_type : std::false_type {};

template <typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};

如果 T 存在成员类型 value_type,则第二个部分会被匹配并继承自 std::true_type,否则它继承自 std::false_type。这样我们就可以使用 has_value_type<T>::value 来判断类型 T 是否具有成员类型 value_type
std::void_t 还可以与其他 STL 组件一起使用,如 std::enable_if_tstd::is_same_v 等,使得模板元编程更加灵活和方便。

2.2. std::uncaught_exceptions()

std::uncaught_exceptions() 是一个 C++17 引入的标准库函数,用于返回当前捕获但未处理的异常数。该函数返回一个整数值,表示当前活动的异常处理器中还有多少个异常没有被处理。
该函数与 std::uncaught_exception() 不同,后者用于检查是否已经有异常被抛出但尚未处理。而 std::uncaught_exceptions() 则可以返回未处理的所有异常数,这对于一些需要在程序中处理多个异常的情况非常有用。
例如,以下代码演示了如何使用 std::uncaught_exceptions() 来处理多个异常:

#include <iostream>
#include <exception>

int main() {
    int count = 0;
    try {
        try {
            throw std::exception();
        } catch (...) {
            ++count;
            throw;
        }
    } catch (...) {
        ++count;
    }

    std::cout << "Number of uncaught exceptions: " << std::uncaught_exceptions() << '\n';
    std::cout << "Total number of exceptions thrown: " << count << '\n';

    return 0;
}

在上面的示例中,我们嵌套了两个 try-catch 块,并在内层的 catch 块中重新抛出异常。当程序运行时,将会抛出两个异常,并在外层的 catch 块中捕获它们。此时,std::uncaught_exceptions() 应该返回 2,表示还有两个未处理的异常。

2.3. Improving std::pair and std::tuple

C++17 中改进了 std::pair 和 std::tuple,具体的改进包括:

  1. 使用结构化绑定来方便地访问 pair 和 tuple 的元素。
  2. 增加了类模板 std::optional,可以方便地表示一个值可能存在也可能不存在的情况。
  3. 增加了 std::apply 函数,可以将一个函数和一个 tuple 组合起来调用,使代码更简洁。
  4. 同时对 pair 和 tuple 进行了优化,使它们在编译器中占用更少的空间。

以下是一些使用 C++17 新特性的示例代码:

  1. 结构化绑定访问 pair 和 tuple 的元素:
std::pair<int, std::string> p{ 42, "hello" };
auto [i, s] = p;
std::cout << "i: " << i << ", s: " << s << std::endl;

std::tuple<int, double, std::string> t{ 42, 3.14, "world" };
auto [x, y, z] = t;
std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;
  1. 使用 std::optional 表示一个值可能存在也可能不存在的情况:
std::optional<std::string> maybe_string = std::nullopt;
if (maybe_string.has_value()) {
    std::cout << "The value is: " << *maybe_string << std::endl;
} else {
    std::cout << "The value is not present." << std::endl;
}

std::optional<int> maybe_int = 42;
if (maybe_int) {
    std::cout << "The value is: " << *maybe_int << std::endl;
} else {
    std::cout << "The value is not present." << std::endl;
}
  1. 使用 std::apply 函数将一个函数和一个 tuple 组合起来调用:
auto add = [](int a, int b) { return a + b; };
std::tuple<int, int> args{ 1, 2 };
std::cout << std::apply(add, args) << std::endl;
  1. 对 pair 和 tuple 进行优化,使它们在编译器中占用更少的空间。

2.4. std::bool_constant

C++17 中新增了一个模板类 std::bool_constant,它可以用来表示一个编译期常量 bool 值。其定义如下:

template <bool B>
struct bool_constant {
static constexpr bool value = B;
using value_type = bool;
using type = bool_constant<B>;
};

其中,B 为 bool 类型的编译期常量,value 表示该常量的值,value_type 表示该常量的类型,type 表示该常量的类型。
使用 std::bool_constant 可以让模板元编程更加清晰和简洁。例如,我们可以将某个函数模板的返回值类型根据一个 bool 值是否为 true 来进行选择,代码如下:

template <typename T, bool B>
struct FooHelper {
using type = T;
};

template <typename T>
struct FooHelper<T, true> {
using type = std::vector<T>;
};

template <typename T, bool B>
using Foo = typename FooHelper<T, B>::type;

Foo<int, false> f1; // f1 的类型是 int
Foo<int, true> f2; // f2 的类型是 std::vector<int>

上述代码中,当 bool 值为 true 时,使用 std::vector 作为返回值类型,否则使用 T 作为返回值类型。其中的 bool 值可以通过 std::bool_constant 来表示,如下所示:

template <typename T, typename Enable = void>
struct Bar {
using type = T;
};

template <typename T>
struct Bar<T, std::enable_if_t<std::is_integral_v<T>>> {
using type = std::bool_constant<(sizeof(T) > sizeof(int))>;
};

typename Bar<char>::type b1; // b1 的类型是 char
typename Bar<long long>::type b2; // b2 的类型是 std::bool_constant<true>

上述代码中,当 T 的类型是整型时,使用 std::bool_constant 来表示一个 bool 值(即 sizeof(T) > sizeof(int)),否则使用 T 作为返回值类型。通过这种方式,我们可以在编译期根据某个类型的特征来进行条件判断。

2.5. std::shared_mutex

C++17 中 std::shared_mutex(无超时)是一个新的互斥量,它与 std::mutex 类似,但可以支持多个读操作和单个写操作同时进行。它的实现方式类似于 std::mutex,但允许多个线程在共享模式下同时访问共享资源。
std::shared_mutex 是一个可重入的、无等待的、非递归的互斥量,可以支持多个读操作同时进行。当有写操作时,所有的读操作都将被阻塞;而当有读操作时,只有写操作会被阻塞。这种特性使得 std::shared_mutex 适用于某些读密集型的场景。
使用 std::shared_mutex 非常简单,与 std::mutex 的使用方式类似。以下是一个示例代码:

#include <iostream>
#include <shared_mutex>
#include <thread>

std::shared_mutex mutex;
int value = 0;

void read_thread() {
    while (true) {
        std::shared_lock lock(mutex);
        std::cout << "read_thread: " << value << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

void write_thread() {
    while (true) {
        std::unique_lock lock(mutex);
        ++value;
        std::cout << "write_thread: " << value << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));
    }
}

int main() {
    std::thread t1(read_thread);
    std::thread t2(write_thread);
    t1.join();
    t2.join();
    return 0;
}

上述代码中,我们定义了一个共享变量 value,以及两个线程 read_thread 和 write_thread。read_thread 用于输出变量的值,write_thread 用于修改变量的值。在 main 函数中,我们启动了这两个线程并等待它们结束。
为了保证多个线程能够安全地访问共享变量,我们使用了 std::shared_mutex 来进行互斥和同步。在 read_thread 中,我们通过 std::shared_lock 对 mutex 进行了共享锁定(读锁),从而使得多个线程可以同时访问 value 的值;在 write_thread 中,我们通过 std::unique_lock 对 mutex 进行了独占锁定(写锁),从而保证了在任何时刻只有一个线程能够修改 value 的值。
需要注意的是,在使用 std::shared_mutex 时,应该避免出现死锁等问题。同时,std::shared_mutex 不支持超时等待,因此不能像 std::mutex 那样使用 try_lock_for 和 try_lock_until 等函数。

2.6. Type traits variable templates

C++17 引入了一种新的 Type Traits 变量模板,可以方便地获取类型信息。这些变量模板定义在头文件 <type_traits> 中,用于查询类型特征。
以下是一些常用的 Type Traits 变量模板:

  1. std::is_void_v:表示是否为 void 类型。
std::cout << std::boolalpha;
std::cout << std::is_void_v<void> << std::endl; // true
std::cout << std::is_void_v<int> << std::endl; // false
  1. std::is_same_v:表示两个类型是否相同。
std::cout << std::boolalpha;
std::cout << std::is_same_v<int, int> << std::endl; // true
std::cout << std::is_same_v<int, double> << std::endl; // false
  1. std::is_integral_v:表示是否为整数类型。
std::cout << std::boolalpha;
std::cout << std::is_integral_v<int> << std::endl; // true
std::cout << std::is_integral_v<double> << std::endl; // false
  1. std::is_floating_point_v:表示是否为浮点数类型。
std::cout << std::boolalpha;
std::cout << std::is_floating_point_v<double> << std::endl; // true
std::cout << std::is_floating_point_v<int> << std::endl; // false
  1. std::is_array_v:表示是否为数组类型。
std::cout << std::boolalpha;
int arr[10];
std::cout << std::is_array_v<int[]> << std::endl; // true
std::cout << std::is_array_v<int> << std::endl; // false
std::cout << std::is_array_v<decltype(arr)> << std::endl; // true

使用 Type Traits 变量模板可以使代码更加简洁和易读,同时也可以提高代码的可靠性和安全性。

2.7. Logical operator type traits

C++17 中新增了三个逻辑操作符的 Type Traits,用于逻辑运算。

  1. std::conjunction:表示与运算(&&),接受零个或多个类型参数,返回一个编译期常量 bool 值,表示所有类型参数的与运算结果。
template <typename... Ts>
using all_is_integral = std::conjunction<std::is_integral<Ts>...>;

std::cout << std::boolalpha;
std::cout << all_is_integral<int, short, long>::value << std::endl; // true
std::cout << all_is_integral<int, double, long>::value << std::endl; // false
  1. std::disjunction:表示或运算(||),接受零个或多个类型参数,返回一个编译期常量 bool 值,表示所有类型参数的或运算结果。
template <typename... Ts>
using any_is_floating_point = std::disjunction<std::is_floating_point<Ts>...>;

std::cout << std::boolalpha;
std::cout << any_is_floating_point<int, short, long>::value << std::endl; // false
std::cout << any_is_floating_point<int, double, long>::value << std::endl; // true
  1. std::negation:表示非运算(!),接受一个类型参数,返回一个编译期常量 bool 值,表示该类型参数的非运算结果。
template <typename T>
using is_not_void = std::negation<std::is_void<T>>;

std::cout << std::boolalpha;
std::cout << is_not_void<void>::value << std::endl; // false
std::cout << is_not_void<int>::value << std::endl; // true

这些逻辑操作符的 Type Traits 可以方便地组合多个类型特征,使代码更加简洁和易读。例如,在一个函数模板中,我们可以使用 conjunction 和 negation 来判断一组类型参数是否都为非空指针类型:

template <typename... Args>
void foo(Args... args) {
    static_assert(std::conjunction_v<std::negation<std::is_pointer<Args>>...>, "All arguments must be non-null pointers.");
    // ...
}

此时,如果有任何一个类型参数不是非空指针类型,就会触发编译期错误。

2.8. Parallel algorithms and execution policies

C++17 中新增了并行算法和执行策略(Execution Policy),以便更好地利用多核处理器的性能。可以在 <algorithm><numeric> 头文件中找到这些新功能。
执行策略定义了一组规则,指定如何将并行算法分配给处理器和线程。C++17 中定义了三种执行策略:

  1. std::execution::seq:顺序执行策略,即不进行并行化。
  2. std::execution::par:并行执行策略,适用于那些可以被有效地划分为独立子任务的算法。
  3. std::execution::par_unseq:混合执行策略,既可以并行化又可以使用 CPU 的向量化指令来加速。

以下是一些常见的并行算法,它们都接受一个执行策略参数作为第一个参数:

  1. std::for_each(std::execution::par, …):对容器的每个元素并行执行操作。
  2. std::sort(std::execution::par, …):在多个线程上并行排序容器。
  3. std::transform_reduce(std::execution::par, …):并行计算由二元操作符组合转换后的所有值的总和。
  4. std::reduce(std::execution::par, …):并行计算所有值的总和或积。
  5. std::copy_if(std::execution::par, …):并行复制满足特定条件的元素。

以下是一个使用 std::for_each 和 std::execution::par 的示例代码:

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>

int main() {
    std::vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::for_each(std::execution::par, v.begin(), v.end(), [](int& x) { x *= 2; });
    for (auto x : v) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
    return 0;
}

上述代码中,我们首先定义了一个包含 10 个整数的向量 v,并使用 std::for_each 和 std::execution::par 对其进行并行处理,将每个元素乘以 2。最后输出修改后的向量 v。
需要注意的是,在使用并行算法时,应该考虑到数据竞争、负载平衡等问题,以充分利用多核处理器的性能。同时,由于并行化涉及到多线程操作,可能会引入一些不确定性和额外开销,因此不适合所有情况。应该根据具体情况选择是否使用并行算法。

2.9. std::clamp()

C++17 中新增了一个方便的函数 std::clamp(),用于将一个值限制在指定的范围内。
std::clamp() 接受三个参数:

  1. value:需要被限制的值。
  2. lo:表示下界,如果 value 小于 lo,则返回 lo。
  3. hi:表示上界,如果 value 大于 hi,则返回 hi。

以下是一个使用 std::clamp() 的示例代码:

#include <iostream>
#include <algorithm>

int main() {
    int x = 10;
    std::cout << std::clamp(x, 1, 5) << std::endl; // 输出 5
    std::cout << std::clamp(x, 15, 20) << std::endl; // 输出 15
    std::cout << std::clamp(x, 1, 20) << std::endl; // 输出 10
    return 0;
}

上述代码中,我们首先定义了一个整数变量 x,然后分别使用不同的上下界对其进行限制,并输出结果。可以看到,std::clamp() 函数将 x 限制在指定的范围内,并返回限制后的值。
需要注意的是,std::clamp() 可以处理不同类型的数据,只要它们可以比较大小。同时,std::clamp() 还支持浮点数和任意可比较类型的范围限制。 在实际开发中,std::clamp() 可以方便地替代一些手写的限制函数。

2.10. Hardware interference size

Hardware interference size(硬件干扰大小)是指在多个处理器上同时执行的程序中,由于缓存一致性协议等原因导致的数据竞争和性能下降的现象。当多个处理器同时访问同一个内存地址时,如果这些处理器的缓存行都缓存了该地址的不同副本,就会发生缓存一致性问题,这可能会导致数据不一致、性能下降甚至系统崩溃。
硬件干扰大小取决于处理器的体系结构和缓存架构等因素,通常是缓存行大小的倍数。在 x86 架构上,缓存行大小通常为 64 字节,因此硬件干扰大小也为 64 字节的倍数。
为了避免硬件干扰问题,需要采取一些策略,例如:

  1. 减少共享数据的使用:减少并发访问同一块数据的次数,可以减少硬件干扰问题的发生。
  2. 使用线程局部存储:将数据存储在线程私有的存储区域中,可以避免多个线程对同一内存地址的竞争。
  3. 采用锁机制:使用锁机制保证同步访问,可以避免多个线程同时访问同一内存地址的问题,从而避免硬件干扰。
  4. 优化数据布局:将经常同时访问的数据放在同一个缓存行中,可以减少硬件干扰的发生。

硬件干扰是并发编程中一个重要的问题,在设计并发程序时需要考虑到这个问题,并采取相应的措施来避免或减少其发生。

2.11. (nothrow-)swappable traits

C++17 中新增了一些与交换(swap)相关的 Type Traits,用于帮助开发者编写更加健壮和高效的代码。

  1. std::is_swappable_v:表示类型是否可交换。
struct A {};
struct B { void swap(B&) noexcept {}; };

std::cout << std::boolalpha;
std::cout << std::is_swappable_v<int> << std::endl; // true
std::cout << std::is_swappable_v<A> << std::endl; // false
std::cout << std::is_swappable_v<B> << std::endl; // true

在上述示例中,我们定义了三个结构体 int、A 和 B,并使用 is_swappable_v 判断它们是否可交换。可以看到,内置类型 int 是可交换的,而 A 不是可交换的,因为它没有定义任何交换操作符。B 是可交换的,因为它定义了一个 noexcept 的 swap 函数。

  1. std::is_nothrow_swappable_v:表示类型是否可无异常交换。
struct C { void swap(C&) noexcept(false) {} };
struct D { void swap(D&) noexcept(true) {} };

std::cout << std::boolalpha;
std::cout << std::is_nothrow_swappable_v<int> << std::endl; // true
std::cout << std::is_nothrow_swappable_v<C> << std::endl; // false
std::cout << std::is_nothrow_swappable_v<D> << std::endl; // true

在上述示例中,我们定义了两个结构体 C 和 D,并使用 is_nothrow_swappable_v 判断它们是否可无异常交换。可以看到,C 不是可无异常交换的,因为它定义了一个可能会抛出异常的 swap 函数,而 D 是可无异常交换的,因它定义了一个 noexcept 的 swap 函数。

  1. std::swap() 和 std::swap_ranges():分别用于交换两个值和交换两个范围内的值。
int a = 1, b = 2;
std::swap(a, b);
std::cout << a << " " << b << std::endl; // 输出 2 1

std::vector<int> v1{ 1, 2, 3 }, v2{ 4, 5, 6 };
std::swap_ranges(v1.begin(), v1.end(), v2.begin());
for (auto x : v1) { std::cout << x << " "; } // 输出 4 5 6
for (auto x : v2) { std::cout << x << " "; } // 输出 1 2 3

在上述示例中,我们首先使用 std::swap() 交换了两个整数变量的值,然后使用 std::swap_ranges() 交换了两个向量的值。
这些 Type Traits 和函数可以帮助开发者更加方便地编写高效、健壮和安全的代码,并提高程序的可靠性和可维护性。

2.12. File system library

C++17 中引入了一个新的标准库,即文件系统库(File System Library),它提供了一组类和函数,用于管理文件和目录等文件系统相关操作。
文件系统库中重要的类和函数包括:

  1. std::filesystem::path:表示文件路径的类,可以进行路径分解、格式化、判断是否为绝对路径或相对路径等操作。
#include <iostream>
#include <filesystem>

int main() {
    std::filesystem::path p("C:/Users/John/Documents/test.txt");
    std::cout << p.filename() << std::endl; // 输出 "test.txt"
    std::cout << p.parent_path() << std::endl; // 输出 "C:/Users/John/Documents"
    std::cout << std::boolalpha << p.is_absolute() << std::endl; // 输出 true
    std::cout << std::boolalpha << p.has_filename() << std::endl; // 输出 true
    return 0;
}

在上述示例中,我们首先定义了一个路径对象 p,然后使用 path 的不同方法获取文件名、父路径、是否为绝对路径、是否存在文件名等信息。

  1. std::filesystem::directory_iterator:表示目录中所有文件和子目录的迭代器。
#include <iostream>
#include <filesystem>

int main() {
    std::filesystem::directory_iterator it("C:/Users/John/Documents");
    for (auto& entry : it) {
        std::cout << entry.path() << std::endl;
    }
    return 0;
}

在上述示例中,我们首先定义了一个目录迭代器 it,然后遍历目录中的所有文件和子目录,并输出每个文件或子目录的路径。

  1. std::filesystem::exists():判断指定路径是否存在文件或目录。
#include <iostream>
#include <filesystem>

int main() {
    std::cout << std::boolalpha << std::filesystem::exists("C:/Users/John/Documents") << std::endl; // 输出 true
    std::cout << std::boolalpha << std::filesystem::exists("C:/Users/John/Documents/test.txt") << std::endl; // 输出 false
    return 0;
}

在上述示例中,我们使用 exists() 函数判断指定路径是否存在文件或目录,并输出结果。
文件系统库提供了更加方便和灵活的文件系统操作方法,可以帮助开发者编写更加健壮和可移植的代码。

2.13. std::string_view

std::string_view 是 C++17 中新增的一个类,它是一个轻量级的字符串类型,用于表示一个不可修改的字符串视图,即对现有的 std::string 或字符数组等数据类型中的一段连续字符的引用。由于 std::string_view 不需要管理内存,因此它可以提高程序的性能和效率,并可以用于许多常见的字符串操作,例如查找、替换、比较等等。
下面是一个简单的示例,演示如何使用 std::string_view

#include <iostream>
#include <string_view>

int main() {
    std::string str = "hello, world!";
    std::string_view str_view(str.c_str(), 5);

    std::cout << "str_view: " << str_view << '\n';
    std::cout << "str_view length: " << str_view.length() << '\n';

    return 0;
}

在上述示例中,我们首先创建了一个名为 strstd::string 对象,然后使用 c_str 函数将其转换为一个 C 风格的字符串,再使用 std::string_view 类型来创建一个名为 str_view 的字符串视图,该视图包含 str 字符串中前五个字符。最后,我们使用 std::cout 打印出 str_viewstr_view 的长度。
需要注意的是,std::string_view 对象并不自己拥有任何内存,因此必须确保源字符串的生命周期要比 std::string_view 对象更长。此外,由于 std::string_view 是不可变的,因此不能修改其所引用的源字符串。

2.14. std::any

std::any 是 C17 中新增的一个标准库类型,用于存储任意类型的值。它的作用类似于 C11 中的 void*,但是比 void* 更加类型安全和易用。
std::any 可以存储任意类型的值,包括基本类型、自定义类型、指针等。使用 std::any 需要包含头文件 <any>。下面是一个简单的示例:

#include <iostream>
#include <any>

int main() {
    std::any a = 42;
    std::cout << std::any_cast<int>(a) << std::endl;

    a = 3.14;
    std::cout << std::any_cast<double>(a) << std::endl;

    a = "hello";
    std::cout << std::any_cast<const char*>(a) << std::endl;

    return 0;
}

在上述代码中,我们首先创建了一个 std::any 类型的变量 a,并将整数 42 存储在其中。然后,使用 std::any_cast 函数将 a 中存储的整数取出,并输出到控制台。接着,将浮点数 3.14 存储在 a 中,并使用 std::any_cast 函数将其取出并输出。最后,将字符串 “hello” 存储在 a 中,并使用 std::any_cast 函数将其取出并输出。
需要注意的是,使用 std::any_cast 函数时,需要指定要转换的类型。如果 std::any 中存储的值的类型与指定的类型不匹配,std::bad_any_cast 异常将会被抛出。因此,在使用 std::any_cast 函数时,应该确保类型匹配。

2.15. std::optional

std::optional 是 C++17 中新增的一个类模板,它提供了一种可选值的语义(可理解为“可能有值也可能没有值”),用于表示一个值可以存在或不存在的情况。与传统的指针或引用类型不同,std::optional 既能够避免空指针异常,又可以在代码中清晰地表达出某些值是可选的。
下面是一个简单的示例,演示如何使用 std::optional

#include <iostream>
#include <optional>

std::optional<int> divide(int a, int b) {
    if (b == 0) {
        return std::nullopt; // 返回一个空对象
    } else {
        return a / b; // 返回一个含有除法结果的 optional 对象
    }
}

int main() {
    auto result = divide(10, 2);
    if (result.has_value()) {
        std::cout << "10 / 2 = " << result.value() << '\n';
    } else {
        std::cout << "Cannot divide by zero!\n";
    }

    return 0;
}

在上述示例中,我们首先定义了一个名为 divide 的函数,该函数接受两个整数,如果第二个整数为零则返回一个空的 std::optional 对象,否则返回一个包含除法结果的 std::optional 对象。然后,我们在 main 函数中调用 divide 函数,并使用 has_value() 方法来检查 std::optional 对象是否包含值,如果有则使用 value() 方法获取其值。
需要注意的是,由于 std::optional 表示可选值,因此它的方法可能会抛出异常。例如,在上述示例中,如果我们试图在空的 std::optional 对象上调用 value() 方法,则会引发 std::bad_optional_access 异常。因此,在使用 std::optional 时务必要小心并遵循最佳实践。

2.16. Polymorphic memory resources

C++17 中引入了 Polymorphic Memory Resources,即多态内存资源。它是一种通用的内存分配器接口,可以用于自定义内存分配策略,以满足不同的应用场景。
多态内存资源的主要接口是 std::pmr::memory_resource,它定义了一组虚函数,包括 allocate()deallocate()is_equal() 等。这些虚函数可以被子类实现以提供不同的内存分配策略。std::pmr::new_delete_resource 是一个内置的多态内存资源,它使用 newdelete 操作符来分配和释放内存。
下面是一个简单的示例,演示如何使用多态内存资源:

#include <iostream>
#include <memory_resource>

int main() {
    std::pmr::memory_resource* mem = std::pmr::new_delete_resource();
    int* p = static_cast<int*>(mem->allocate(sizeof(int)));
    *p = 42;
    std::cout << *p << std::endl;
    mem->deallocate(p, sizeof(int));
    return 0;
}

在上述代码中,我们首先使用 std::pmr::new_delete_resource() 函数创建了一个内置的多态内存资源。然后,使用 allocate() 函数从该内存资源中分配了一个 int 类型的内存空间,并将其初始化为 42。最后,使用 deallocate() 函数释放了该内存空间。
需要注意的是,多态内存资源只是一个接口,它不能直接用于内存分配。我们需要使用子类来实现该接口,以提供具体的内存分配策略。例如,可以使用 std::pmr::monotonic_buffer_resource 类来实现一个基于缓冲区的内存分配器,该分配器可以在一个预先分配的缓冲区中分配内存。
如果申请的内存特别大,或者有多种内存申请策略,可以采用上述方法,统都一内存申请接口,将具体的内存申请策略移动到分配器中来完成。

2.17. Mathematical special functions

C++17 是 C++ 的一个版本,它引入了许多新的特性和改进,其中包括对数学特殊函数的支持。C++17中引入了 头文件中的一些特殊函数,例如 std::tgamma() 用于计算伽马函数,std::j0() 和 std::jn() 用于计算Bessel函数等等。这些函数可以很方便地进行数值计算和科学应用。
是的,除了 头文件中提供的数学特殊函数之外,C++ 还提供了其他一些在数学、科学和工程领域广泛使用的特殊函数。
例如,在 头文件中,C++ 提供了许多复数相关的特殊函数,如正弦、余弦、指数和对数。

2.18. Major portion of C11 standard library

C++17,兼容C11的标准库。
C11标准库包括以下主要部分:

  1. 核心库(Core Library):包括常用的数据类型、数学函数、字符处理函数、文件操作函数、动态内存分配函数等。
  2. 字符串库(String Library):包括字符串操作函数,如字符串拷贝、字符串比较、字符串查找等。
  3. 输入输出库(Input/Output Library):包括文件输入输出函数、标准输入输出函数、格式化输出函数等。
  4. 时间库(Time Library):包括日期和时间处理函数,如获取当前时间、日期格式化等。
  5. 数学库(Math Library):包括常用的数学函数,如三角函数、指数函数、对数函数等。
  6. 随机数库(Random Number Library):包括生成随机数的函数。
  7. 宽字符库(Wide Character Library):包括宽字符类型和宽字符操作函数,如宽字符输入输出、宽字符字符串操作等。
  8. 原子操作库(Atomic Operations Library):包括原子操作函数,用于实现多线程同步和互斥。
  9. 多线程库(Thread Library):包括多线程相关的函数,如线程创建、线程同步、线程互斥等。

总之,C11标准库提供了丰富的函数和数据类型,可以帮助开发人员更加方便地进行编程。无论是学习C语言还是进行实际开发,掌握C11标准库都是非常重要的。

2.19. Splicing Maps and Sets

“Splicing Maps and Sets” 是 C++17 中新增的功能,它允许将一个 mapset 中的元素移动到另一个 mapset 中,而无需复制或销毁它们。这样做可以提高效率并减少内存使用。
具体来说,可以使用 extract() 方法将指定键或迭代器所指的元素从一个 mapset 中移除,并返回一个包含该元素的 node_type 对象。然后可以使用 insert() 方法将该 node_type 对象插入到另一个 mapset 中,以实现元素的移动。
下面是一个使用 extract()insert() 方法实现 map 元素移动的示例:

#include <iostream>
#include <map>

int main() {
    // 创建两个 map 对象
    std::map<int, std::string> src_map = {{1, "one"}, {2, "two"}, {3, "three"}};
    std::map<int, std::string> dest_map = {{4, "four"}, {5, "five"}};

    // 将 src_map 中第一个元素移动到 dest_map 中
    auto node = src_map.extract(src_map.begin());
    dest_map.insert(std::move(node));

    // 输出结果
    std::cout << "Source Map:\n";
    for (const auto& [key, value] : src_map) {
        std::cout << key << ": " << value << "\n";
    }

    std::cout << "Destination Map:\n";
    for (const auto& [key, value] : dest_map) {
        std::cout << key << ": " << value << "\n";
    }

    return 0;
}

输出结果如下:

Source Map:
2: two
3: three
Destination Map:
1: one
4: four
5: five

可以看到,原先在 src_map 中的第一个元素已经被移动到了 dest_map 中。

2.20. return type of emplace* functions of some containers changed from void to reference

在C++17标准中,对一些STL容器的emplace*函数进行了修改,使得它们的返回类型从void(即什么都不返回)变为对插入元素类型的引用。这些容器包括std::vector、std::deque和std::list等。
以std::vector为例,旧版本中的emplace_back函数是这样定义的:

void emplace_back(Args&&... args);

这意味着调用该函数时会将传递进去的参数构造成一个元素插入到vector中,但函数本身并不返回任何值。现在,在C++17中,emplace_back函数被重新定义如下:

template <class... Args>
decltype(auto) emplace_back(Args&&... args);

可以看到,函数返回类型使用了decltype(auto),这表示返回类型与插入元素的类型相同,即对插入元素的引用。也就是说,调用emplace_back函数后,我们可以立即对插入的元素进行操作,例如:

std::vector<int> vec;
int& ref = vec.emplace_back(42);
ref = 24;

这里的ref是对插入的元素的引用,因此我们可以直接对其进行修改。同时,由于emplace_back函数返回对插入元素的引用,我们还可以将多个emplace_back函数链接起来进行链式调用:

std::vector<std::pair<int, std::string>> vec;
vec.emplace_back(1, "one").second = "uno";

这里的second是std::pair的一个成员变量,因此我们可以直接通过返回值对其进行修改。
总之,C++17中对emplace*函数的修改可以使得插入元素变得更加高效和方便。

2.21. std::variant

std::variant是C++17中引入的一个标准库类型,它表示一种可以存储不同类型值的联合(Union)。
与传统的Union相比,std::variant具有更好的类型安全性和更多的功能。在std::variant中,用户可以指定允许存储的类型,例如:

std::variant<int, double, std::string> myVariant;

这里的myVariant可以存储int、double或std::string类型的值。当我们需要向std::variant中存储一个值时,可以使用std::visit或者std::get来进行操作,例如:

// 向variant中存储int类型的值
myVariant = 42;

// 使用std::get获取variant中存储的值
int intValue = std::get<int>(myVariant);

// 使用std::visit对variant中存储的值进行操作
std::visit([](auto&& arg) {
    std::cout << "The value is: " << arg << '\n';
}, myVariant);

需要注意的是,当我们使用std::get获取一个不存在的类型时,会抛出std::bad_variant_access异常。此外,如果我们需要检查std::variant当前存储的值的类型,可以使用std::holds_alternative函数,例如:

if (std::holds_alternative<int>(myVariant)) {
    // variant中当前存储的是int类型的值
}

总之,std::variant提供了一种灵活且类型安全的方式来存储多个类型的值,并通过std::visit和std::get等函数提供了方便的操作方式。

2.22. [std::make_from_tuple()]

std::make_from_tuple()是C++17中引入的一个标准库函数,它可以从一个tuple对象中构造一个新的对象。
具体来说,如果我们有一个tuple对象和一个可调用对象(例如函数指针、函数对象等),并且这个可调用对象接受与tuple对象中元素类型一一对应的参数,则可以使用std::make_from_tuple()将tuple对象中的元素作为参数传递给可调用对象,并返回一个新的对象,例如:

// 定义一个可调用对象
struct MyStruct {
int x;
std::string y;
double z;

MyStruct(int x, const std::string& y, double z)
: x(x), y(y), z(z) {}
};

// 创建一个tuple对象
std::tuple<int, std::string, double> myTuple(42, "hello", 3.14);

// 使用make_from_tuple从tuple对象中构造新对象
MyStruct myObj = std::make_from_tuple<MyStruct>(myTuple);

上述代码中,我们定义了一个名为MyStruct的结构体,表示一个包含三个成员变量的对象。然后,我们创建了一个tuple对象myTuple,其中包含三个与MyStruct的成员变量类型相同的元素。最后,我们使用std::make_from_tuple()将myTuple中的元素作为参数传递给MyStruct的构造函数,并返回了一个新的MyStruct对象myObj。
需要注意的是,如果可调用对象的参数数量小于tuple中元素的数量,则会导致编译时错误。此外,如果tuple中元素的类型无法隐式转换为可调用对象的参数类型,则也会导致编译时错误。

2.23. std::has_unique_object_representations

std::has_unique_object_representations是C++17中引入的一个标准库类型特征(trait),用于判断指定类型的对象是否具有唯一的对象表示形式(unique object representations)。
对象表示形式是指一个对象在内存中的位模式,包括对象的值和所有成员变量的值。如果一个类型的对象具有唯一的对象表示形式,则可以安全地使用memcpy等函数进行内存拷贝操作。
在C++17之前,无法使用标准库来判断一个类型是否具有唯一的对象表示形式,而只能通过手动检查类型的定义来确定。在C17中,我们可以使用std::has_unique_object_representations来自动检查类型是否具有唯一的对象表示形式,例如:

#include <iostream>
#include <type_traits>

struct MyStruct {
int x;
double y;
};

int main() {
    std::cout << std::boolalpha;
    std::cout << "MyStruct has unique object representations? "
        << std::has_unique_object_representations<MyStruct>::value
        << '\n';
}

上述代码中,我们定义了一个名为MyStruct的结构体,并使用std::has_unique_object_representations来检查它是否具有唯一的对象表示形式。由于MyStruct的所有成员变量都具有唯一的对象表示形式,因此该程序会输出true。
需要注意的是,std::has_unique_object_representations仅适用于POD类型(Plain Old Data),也就是没有任何特殊成员函数或虚函数的类型。如果一个类型具有特殊成员函数或虚函数,则可能没有唯一的对象表示形式,因此std::has_unique_object_representations会返回false。

2.24. std::gcd() and std::lcm()

std::gcd()和std::lcm()是C++17中引入的两个标准库函数,分别用于计算两个整数的最大公约数(greatest common divisor)和最小公倍数(least common multiple)。
其中,std::gcd()函数接受两个整数参数,并返回它们的最大公约数,例如:

#include <iostream>
#include <numeric>

int main() {
    int a = 12, b = 18;
    std::cout << "GCD of " << a << " and " << b << " is "
              << std::gcd(a, b) << '\n';
}

上述代码中,我们使用std::gcd()函数计算了12和18的最大公约数,并输出结果6。
与此类似,std::lcm()函数也接受两个整数参数,并返回它们的最小公倍数,例如:

#include <iostream>
#include <numeric>

int main() {
    int a = 12, b = 18;
    std::cout << "LCM of " << a << " and " << b << " is "
              << std::lcm(a, b) << '\n';
}

上述代码中,我们使用std::lcm()函数计算了12和18的最小公倍数,并输出结果36。
需要注意的是,std::gcd()和std::lcm()函数都要求输入的参数为整数类型,且必须支持取模操作(即支持%运算符)。如果输入的参数为负数,则std::gcd()和std::lcm()会自动将其转换为正数进行计算。此外,如果输入的参数为0,则std::gcd()和std::lcm()会返回0。

2.25. [std::not_fn]

std::not_fn是C++17中引入的一个标准库函数,用于创建一个返回bool类型值的函数对象,并将其与其他函数对象组合使用时进行逻辑非(logical not)运算。
具体来说,std::not_fn接受一个可调用对象f,并返回一个新的函数对象g。当我们对g(x)进行调用时,它会返回!f(x)的结果,即f(x)的逻辑非结果。
例如,考虑以下代码:

#include <iostream>
#include <functional>

int main() {
    std::function<bool(int)> f = [](int x) { return x > 0; };
    auto g = std::not_fn(f);

    std::cout << "f(42) = " << f(42) << '\n';
    std::cout << "g(42) = " << g(42) << '\n';
}

上述代码中,我们定义了一个名为f的函数对象,它接受一个整数参数并返回bool类型的结果。然后,我们使用std::not_fn(f)创建了一个新的函数对象g,并调用g(42)进行测试。由于f(42)返回true,因此g(42)返回false。
需要注意的是,std::not_fn创建的函数对象可以与其他函数对象结合使用,如std::transform、std::for_each等STL算法中的函数对象。同时,std::not_fn也可以用于创建谓词函数,例如:

#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>

int main() {
    std::vector<int> vec{1, 2, 3, -4, -5};
    auto is_negative = std::not_fn(std::greater<int>());
    auto count = std::count_if(vec.begin(), vec.end(), is_negative);
    std::cout << "There are " << count << " negative numbers in vec\n";
}

上述代码中,我们使用std::not_fn创建了一个谓词函数is_negative,它返回输入值是否小于等于0的结果。然后,我们使用std::count_if统计vec中满足is_negative条件的元素个数,并输出结果。

2.26. Elementary string conversions*

Elementary string conversions是C++17中引入的一组标准库函数,用于在不同进制之间进行字符串和数字类型的转换。
具体来说,这些函数包括:

  • std::to_chars():将整数类型值转换为字符序列,并返回指向字符序列结尾的指针。
  • std::from_chars():将字符序列转换为整数类型值,并返回指向字符序列结尾的指针。
  • std::to_integer():将字符序列转换为整数类型值,并抛出std::invalid_argument或std::out_of_range异常(如果转换失败)。
  • std::to_unsigned():将字符序列转换为无符号整数类型值,并抛出std::invalid_argument或std::out_of_range异常(如果转换失败)。
  • std::to_float():将字符序列转换为浮点型数值,并抛出std::invalid_argument或std::out_of_range异常(如果转换失败)。

例如,考虑以下代码:

#include <iostream>
#include <charconv>

int main() {
    // 将整数值转换为十六进制字符串
    int x = 255;
    char buffer[16];
    auto [p, ec] = std::to_chars(buffer, buffer + sizeof(buffer), x, 16);
    if (ec != std::errc()) {
        std::cout << "Error: to_chars failed\n";
        return 1;
    }
    *p = '\0';

    std::cout << "x in hex is " << buffer << '\n';

    // 将十六进制字符串转换为整数值
    int y;
    auto [p2, ec2] = std::from_chars(buffer, p, y, 16);
    if (ec2 != std::errc()) {
        std::cout << "Error: from_chars failed\n";
        return 1;
    }

    std::cout << "y is " << y << '\n';
}

上述代码中,我们使用std::to_chars将整数值255转换为十六进制字符串,并存储在字符数组buffer中。然后,我们使用std::from_chars将该十六进制字符串解析为整数值,并将结果存储在变量y中。
需要注意的是,上述函数中的参数和返回值都采用了C++17中引入的std::tuple类型,可以通过C++17中的结构化绑定语法(structured binding syntax)方便地获取其中的元素。此外,如果转换失败,则函数会返回一个错误码ec,可以通过比较它与std::errc()是否相等来判断转换是否成功。

2.27. std::shared_ptr and std::weak_ptr with array support

C++17对std::shared_ptr和std::weak_ptr进行了扩展,使它们可以更方便地管理动态数组。
具体来说,C++17中的std::shared_ptr和std::weak_ptr支持使用std::make_shared和std::make_unique函数创建动态数组,并支持使用operator[]访问数组元素。例如:

#include <memory>
#include <iostream>

int main() {
    // 创建包含3个整数的动态数组
    auto sptr = std::make_shared<int[]>(3);
    sptr[0] = 10;
    sptr[1] = 20;
    sptr[2] = 30;

    // 使用weak_ptr获取shared_ptr并输出数组元素
    std::weak_ptr<int[]> wptr(sptr);
    if (auto shared = wptr.lock()) {
        for (int i = 0; i < 3; ++i) {
            std::cout << shared[i] << ' ';
        }
        std::cout << '\n';
    }

    return 0;
}

上述代码中,我们使用std::make_shared创建了一个包含3个整数的动态数组,并将其存储在std::shared_ptr对象sptr中。然后,我们使用operator[]访问数组元素,并将值存储在数组中。接下来,我们使用std::weak_ptr获取sptr的弱引用wptr,并通过wptr.lock()获取与之关联的std::shared_ptr对象,最后输出数组元素的值。
需要注意的是,当我们使用std::make_shared创建动态数组时,必须在尖括号中指定数组元素的类型及其数量。此外,使用operator[]访问数组元素时,需要确保下标不越界,否则会导致未定义行为。
同时,C++17中的std::shared_ptr和std::weak_ptr还支持使用自定义删除器(deleter)管理动态数组。如果我们需要在数组被释放时执行一些额外的操作,可以通过提供自定义删除器实现,例如:

#include <memory>
#include <iostream>

int main() {
    // 创建包含3个字符串的动态数组,指定自定义删除器
    auto deleter = [](std::string* arr) {
        for (int i = 0; i < 3; ++i) {
            std::cout << "Deleting: " << arr[i] << '\n';
        }
        delete[] arr;
    };
    std::shared_ptr<std::string[]> sptr(new std::string[3], deleter);

    // 使用weak_ptr获取shared_ptr并输出数组元素
    std::weak_ptr<std::string[]> wptr(sptr);
    if (auto shared = wptr.lock()) {
        shared[0] = "hello";
        shared[1] = ", ";
        shared[2] = "world!";
        for (int i = 0; i < 3; ++i) {
            std::cout << shared[i];
        }
        std::cout << '\n';
    }

    return 0;
}

上述代码中,我们创建了一个包含3个字符串的动态数组,每个字符串都存储在堆上。然后,我们指定一个自定义删除器,在数组被释放时输出每个字符串的值。接下来,我们使用std::weak_ptr获取sptr的弱引用wptr,并通过wptr.lock()获取与之关联的std::shared_ptr对象。最后,我们输出数组元素的值,并在程序结束时执行自定义删除器中的操作。

2.28. std::scoped_lock

std::scoped_lock是C++17中引入的一个标准库类型,用于在多个互斥锁上进行加锁操作。
具体来说,std::scoped_lock接受一个或多个互斥锁对象,并在构造函数中对它们进行加锁,在析构函数中对它们进行解锁。这样可以确保在代码块中同时访问多个互斥锁时,不会发生死锁的情况。
例如,考虑以下代码:

#include <iostream>
#include <mutex>
#include <thread>

int main() {
    std::mutex m1, m2;
    std::thread t1([&]() {
        std::scoped_lock lock(m1, m2);
        std::cout << "Thread 1 locked m1 and m2\n";
    });
    std::thread t2([&]() {
        std::scoped_lock lock(m2, m1);
        std::cout << "Thread 2 locked m2 and m1\n";
    });
    t1.join();
    t2.join();

    return 0;
}

上述代码中,我们定义了两个互斥锁对象m1和m2,并在两个线程中分别对它们进行加锁操作。由于t1和t2加锁的顺序不同,可能会导致死锁的情况发生。为了避免死锁,我们使用std::scoped_lock对多个互斥锁进行加锁操作。由于std::scoped_lock在构造函数中对所有互斥锁对象进行加锁,在析构函数中对它们进行解锁,因此可以确保在代码块中同时访问多个互斥锁时不会发生死锁的情况。
需要注意的是,std::scoped_lock在构造函数中对所有互斥锁进行加锁时,会使用std::lock()算法对它们进行排序,以避免死锁。此外,std::scoped_lock也支持在加锁时指定锁的等待策略(lock policy),例如std::defer_lock、std::try_to_lock和std::adopt_lock等。

2.29. std::byte

std::byte是C++17中引入的一个标准库类型,用于表示原始字节(raw byte)。
具体来说,std::byte是一个枚举类型,包含0到255之间的所有整数值。它可以用于表示二进制数据,并支持按位运算和比较操作。例如:

#include <iostream>
#include <cstring>

int main() {
    // 创建一个包含10个字节的缓冲区
    std::byte buffer[10];

    // 将前5个字节设置为0xff,后5个字节设置为0x00
    std::memset(buffer, static_cast<int>(std::byte{0xff}), 5);
    std::memset(buffer + 5, static_cast<int>(std::byte{0x00}), 5);

    // 输出缓冲区中的字节值
    for (auto b : buffer) {
        std::cout << std::hex << std::to_integer<int>(b) << ' ';
    }
    std::cout << '\n';

    // 按位取反缓冲区中的所有字节
    for (auto& b : buffer) {
        b = ~b;
    }

    // 输出缓冲区中的字节值
    for (auto b : buffer) {
        std::cout << std::hex << std::to_integer<int>(b) << ' ';
    }
    std::cout << '\n';

    return 0;
}

上述代码中,我们创建了一个包含10个字节的std::byte缓冲区,并使用std::memset对前5个字节设置为0xff,后5个字节设置为0x00。然后,我们使用std::to_integer将每个std::byte值转换为int类型,并以十六进制格式输出其值。接着,我们对缓冲区中的所有字节按位取反,并再次输出其值。
需要注意的是,std::byte本质上是一个无符号整数类型,但它并不支持算术运算。此外,std::byte也可以与其他整数类型之间进行隐式转换,例如:

#include <iostream>
#include <cstdint>

void print_byte(std::byte b) {
    std::cout << std::hex << std::to_integer<int>(b) << '\n';
}

int main() {
    std::byte b1{0x12};
    std::byte b2 = static_cast<std::byte>(0x34);
    std::byte b3 = static_cast<std::byte>(static_cast<uint8_t>(0x56));

    print_byte(b1);
    print_byte(b2);
    print_byte(b3);

    return 0;
}

上述代码中,我们定义了三个std::byte对象b1、b2和b3,并分别初始化为0x12、0x34和0x56。同时,我们定义了一个函数print_byte,用于以十六进制格式输出std::byte对象的值。需要注意的是,虽然std::byte本质上是一个无符号整数类型,但它不能直接赋值或初始化为其他整数类型的值,必须通过显示类型转换进行转换。

2.30. std::is_aggregate

这个问题应该是在C++17标准中,使用以下代码进行测试:

struct Person {
std::string name;
int age;
};

bool is_aggr = std::is_aggregate<Person>::value;

在上述代码中,我们定义了一个名为Person的结构体,包含一个std::string类型的成员变量name和一个int类型的成员变量age。然后,我们使用std::is_aggregate模板来判断Person类型是否是聚合体,并将结果保存到is_aggr变量中。
需要注意的是,在C++17标准中,std::string被定义为字面值类型之一,因此满足聚合体的要求,可以作为聚合体的成员。因此,在这种情况下,is_aggr变量的值应该为true,表示Person类型是一个聚合体。
但是,在C++20标准中,std::string不再是字面值类型之一,因此不能作为聚合体的成员。因此,在C++20标准中,上述代码将无法通过编译,会提示错误信息。
综上所述,要确定Person是否是一个聚合体,需要考虑具体的C++标准和std::string的定义。在C17标准及以前版本中,std::string是一个字面值类型,因此可以作为聚合体的成员;而在C++20标准中,std::string不是字面值类型,不能作为聚合体的成员。

2.31. std::hash<std::filesystem::path>

std::hash是C++标准库中的一个哈希函数模板,用于将任意类型的数据转换为哈希值(整数)。而std::filesystem::path是C++17中新增加的文件系统路径类,用于表示文件路径和文件名。
在C++17之前,如果要在无序容器(如std::unordered_map)中使用std::filesystem::path对象,需要自行实现哈希函数。但是,在C++17标准中,std::hash<std::filesystem::path>被添加到了标准库中,可以直接对std::filesystem::path进行哈希操作,例如:

#include <iostream>
#include <filesystem>

int main() {
    std::filesystem::path p("/usr/bin/ls");
    std::size_t h = std::hash<std::filesystem::path>{}(p);
    std::cout << "Hash of " << p << " is " << h << std::endl;
    return 0;

在上述代码中,我们定义了一个std::filesystem::path对象p,表示Linux系统中的ls命令。然后,使用std::hash<std::filesystem::path>对其进行哈希操作,将结果保存到变量h中,并输出到控制台。
需要注意的是,对于std::filesystem::path对象的哈希操作,其实现可能会因操作系统、文件系统等因素而有所不同。因此,在使用std::hash<std::filesystem::path>进行哈希操作时,应该明确其实现机制和特性,并根据具体的需求和场景选择适当的哈希函数。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值