C++20新语法

本文详细介绍了C++20的新语言特性,包括允许Lambda捕获[=, this]、VA_OPT宏、指定初始化器等61项内容。这些特性增强了语言的灵活性和通用性,如支持模板参数列表的lambda表达式、类模板参数推导等,同时修正了一些旧标准中的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

New language features

1. Allow Lambda capture [=, this]

在C++20标准中,允许Lambda表达式使用 [=, this] 这样的语法进行捕获。这种语法称为“复合捕获”(compound capture),表示同时对this指针和所有父作用域的自动变量进行值捕获。
具体来说,当我们使用 [=, this] 进行复合捕获时,Lambda表达式会自动捕获当前对象的this指针,并以值的方式复制到Lambda表达式的闭包中。与此同时,Lambda表达式还将自动捕获所有父作用域中的自动变量,并以值的方式复制到闭包中。
例如,考虑以下代码:

class A {
public:
void foo() {
    int x = 1;
    auto lambda = [=, this]() {
        std::cout << "x = " << x << ", y = " << y << std::endl;
    };
    lambda();
}
private:
int y = 2;
};

int main() {
    A a;
    a.foo();
    return 0;
}

在上述代码中,我们定义了一个类A,其中包含一个成员变量y和一个成员函数foo。在foo函数中,我们定义了一个自动变量x并将其赋值为1。然后,我们使用 [=, this] 的复合捕获语法,定义了一个Lambda表达式lambda,并在其中输出x和y的值。最后,我们调用lambda函数,输出结果。
需要注意的是,复合捕获语法 [=, this] 只适用于捕获当前对象的this指针。如果我们想要捕获父作用域中的某个变量,仍然需要使用[]中的单独捕获语法。例如,[=, x] 表示对所有父作用域的自动变量进行值捕获,并额外对变量x进行值捕获。

2. VA_OPT

VA_OPT是C++20中引入的一个预处理器宏,用于控制可变参数宏(variadic macro)中可选参数的展开方式。
在C++17及之前,我们通常使用类似下面的方式来实现带有可选参数的宏:

#define LOG(message, ...) \
do { \
fprintf(stderr, "%s:%d: " message "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
} while (0)

在上述代码中,我们定义了一个LOG宏,其后跟一个message参数和一个可变参数VA_ARGS。通过在VA_ARGS前面加上##,可以确保当可变参数列表为空时,不会出现额外的逗号。这种技巧称为“空参数占位符”(empty argument placeholder)。
然而,这种做法并不能满足所有情况。例如,如果我们想要将多个可选参数包装到一个结构体中,并且在宏中使用统一的语法来访问这些参数,就需要更灵活的展开方式。
VA_OPT宏就是为了解决这个问题而引入的。通过在可选参数后面添加VA_OPT(…),我们可以使得这些参数只在存在时才被展开。例如:

#define LOG2(message, ...) \
do { \
struct LogContext { __VA_OPT__(public: __VA_ARGS__) }; \
LogContext context; \
fprintf(stderr, "%s:%d: " message "\n", __FILE__, __LINE__); \
} while (0)

在上述代码中,我们将可选参数包装到了一个结构体LogContext中,并使用VA_OPT(public: VA_ARGS)来指示当可选参数存在时,在结构体中添加public访问修饰符。这样可以保证所有的可选参数都以统一的方式被处理。
需要注意的是,VA_OPT宏只在可变参数宏中有效,且必须与VA_ARGS一起使用。此外,某些编译器可能不支持VA_OPT宏,因此在使用时需要根据具体情况进行判断和测试。

3. Designated initializers

C++20 中引入了类似于 C99 的 designated initializers,可以在初始化复杂的数据结构时更加方便地指定特定成员的初始值。在 C++20 中,可以通过使用花括号和点号来指定数据结构中具体的成员,例如:

struct Point {
int x;
int y;
};

Point p = {.x = 10, .y = 20}; // 使用 designated initializers 初始化 Point
上面的代码中,我们使用了点号来指定 Point 结构体中 x 和 y 成员的初始值,这样就可以更加清晰地初始化结构体。
对于数组和聚合类型,也可以使用类似的语法来指定元素或成员的初始值。例如:

int arr[3] = {[0] = 1, [2] = 3}; // 指定数组中第一个和第三个元素的初始值
struct Rectangle {
int width;
int height;
};

Rectangle r = {.width = 10, .height = 20}; // 使用 designated initializers 初始化 Rectangle
注意,C++20 中的 designated initializers 并不是一个标准化的特性,因此不是所有编译器都支持该特性。如果需要在代码中使用 designated initializers,请先检查编译器是否支持。

4. template-parameter-list for generic lambdas

C++14 引入了 lambda 表达式,允许我们在代码中创建匿名函数。在 C++20 中,lambda 表达式得到了增强,其中之一就是支持了模板参数列表。
使用模板参数列表,我们可以使 lambda 表达式更加通用化,从而支持更多的类型。模板参数列表出现在参数列表之前,并以尖括号包裹模板参数,例如:
auto lambda = [](T x, T y) { return x + y; };
上面的代码定义了一个通用的 lambda 表达式,它接受两个相同类型的参数,并返回它们的和。在尖括号中指定模板参数 T,使 lambda 表达式能够适应不同的类型。
在使用 lambda 表达式时,也可以显式地传递模板参数,例如:
int result = lambda.operator()(1, 2); // 使用 int 类型调用 lambda 表达式
这样就可以使用不同的类型调用 lambda 表达式,从而获得更大的灵活性和通用性。

5. Default member initializers for bit-fields

C++11 中引入了默认成员初始化器(default member initializers)的概念,允许我们在定义类成员变量时为它们提供默认值。在 C++20 中,这个特性得到了增强,可以支持位域(bit-field)类型的成员变量进行默认初始化。
对于位域类型的成员变量,我们可以使用类似于以下的语法来指定默认初始值:

struct Flags {
unsigned int isEnabled : 1 = 1; // 指定默认值为 1
unsigned int isUpgraded : 1 = 0; // 指定默认值为 0
};

上面的代码中,我们定义了一个名为 Flags 的结构体,其中包含两个位域类型的成员变量 isEnabled 和 isUpgraded。在定义这些成员变量时,我们使用 = 1 和 = 0 来指定它们的默认值。
注意,C++20 中的默认成员初始化器对于位域类型的成员变量是可选的,如果没有提供默认值,则这些成员变量将按照标准行为进行初始化。此外,C++20 还引入了其他一些有关位域类型成员变量的改进和修复,例如对于位域类型成员变量的布局规则进行了更加具体的规定,以及修复了一些之前的实现问题。

6. Initializer list constructors in class template argument deduction

C++17 引入了类模板参数推导(class template argument deduction)的特性,可以使得在实例化模板类时不需要显式传递类型参数。在 C++20 中,这个特性得到了增强,支持使用初始化列表构造函数进行类模板参数推导。
假设我们有一个简单的 vector 类模板和一个 Point 结构体:

template<typename T>
struct vector {
// ...
};

struct Point {
int x;
int y;
Point(int x, int y) : x(x), y(y) {}
};

在 C++17 中,我们可以使用以下语法来通过类模板参数推导创建 vector 对象:
auto v = vector{1, 2, 3}; // 相当于 vector(std::initializer_list{1, 2, 3})
而在 C++20 中,我们也可以使用以下语法来通过类模板参数推导创建带有初始化列表构造函数的类对象:
auto p = vector{{1, 2}, {3, 4}}; // 相当于 vector(std::initializer_list{{1, 2}, {3, 4}})
上面的代码中,我们将两个 Point 对象作为元素传递给 vector 的初始化列表构造函数,并使用类模板参数推导推导出 vector 的类型。
需要注意的是,对于非模板类的初始化列表构造函数,我们可以省略花括号。但是对于模板类来说,必须使用多重花括号来区分类模板参数和初始化列表。

7. const&-qualified pointers to members

在 C++11 中,我们可以使用指向成员的指针(pointer-to-member)来访问类的成员。在 C++20 中,我们可以将指向成员的指针声明为 const& 类型的常量引用,从而获得更多的灵活性和可读性。
假设我们有一个名为 Point 的类,其中包含两个成员变量 x 和 y:

class Point {
public:
int x;
int y;
};

我们可以定义一个指向成员变量 x 的指针,并将其声明为 const& 类型的常量引用,如下所示:
const auto& xp = &Point::x; // 将指向 Point 类的 x 成员变量的指针声明为 const&
这样我们就可以在代码中使用 xp 来访问 Point 对象的 x 成员变量,例如:

Point p{1, 2};
std::cout << p.*xp << std::endl; // 输出:1

同样地,我们也可以声明指向成员函数的指针为 const& 类型的常量引用,例如:

class MyClass {
public:
    void foo(int) {}
    void bar() const {}
};
auto& fp = &MyClass::foo; // 将指向 MyClass 的 foo 成员函数的指针声明为 const&
auto& bp = &MyClass::bar; // 将指向 MyClass 的 bar 成员函数的指针声明为 const&

这样我们就可以在代码中使用 fp 和 bp 来访问 MyClass 对象的对应成员函数,例如:

MyClass obj;
(obj.*fp)(42); // 调用 obj 的 foo 成员函数,并将参数 42 传递给它
(obj.*bp)(); // 调用 obj 的 bar 成员函数

需要注意的是,在使用 const&-qualified 指向成员的指针时,必须确保被访问的成员变量或成员函数也是 const-qualified 的。否则会导致编译错误。

8. Concepts

C++20 引入了 Concepts 的概念,可以用来指定类型或模板的要求。Concepts 可以理解为是一种约束条件,用来限制模板参数的类型范围,提高代码的可读性和可维护性。
使用 Concepts,我们可以在定义模板时指定一个或多个 Concept 作为其模板参数的限制条件。例如,以下代码定义了一个名为 Printable 的 Concept,用于约束所有具有 print() 函数的类型:
template
concept Printable = requires(T t) { t.print(); };
上面的代码中,我们使用 requires 关键字来指定 Printable 的要求,即类型 T 必须具有一个无参 print() 函数。然后,我们可以在定义模板时使用 Printable,如下所示:

template<Printable T>
void print(const T& t) {
    t.print();
}

在上面的代码中,我们定义了一个名为 print() 的模板函数,它接受一个 Printable 类型的参数并调用其 print() 函数。这样,通过使用 Concepts,我们就可以在编译期间检查模板参数是否满足特定的约束条件,从而提高代码的健壮性和可靠性。
除了内置的 Concepts(例如 Same, ConvertibleTo 等),开发者还可以自定义 Concepts 来满足特定需求,例如约束类、函数等。需要注意的是,Concepts 是 C++20 中的一个新特性,不是所有编译器都支持。

9. Lambdas in unevaluated contexts

在 C++ 中,有一些上下文是不会对表达式进行求值的(unevaluated contexts),例如类型或模板参数推导、sizeof 运算符等。在 C++20 中,我们可以在这些 unevaluated contexts 中使用 lambda 表达式,从而获得更大的灵活性和可读性。
假设我们要为一个函数指定返回类型,并且该返回类型取决于函数的参数类型。在 C++11 中,我们需要使用尾置返回类型语法来实现:

template<typename T>
auto func(const T& t) -> decltype(t.size()) {
    return t.size();
}

在 C++20 中,我们可以使用 unevaluated context 中的 lambda 表达式来简化这个过程:

template<typename T>
auto func(const T& t) {
    return []<typename U>(const U& u) -> decltype(u.size()) { return u.size(); }(t);
}

上面的代码中,我们定义了一个 lambda 表达式,它接受一个参数 u,并返回 u.size() 的结果。然后,我们立即调用该 lambda 表达式,并将函数的参数 t 传递给它。由于 lambda 表达式出现在 unevaluated context 中,因此它不会被执行,只有其返回类型会被推导出来。这样,我们就可以避免使用尾置返回类型语法,使代码更加简洁和易读。
需要注意的是,在 unevaluated context 中使用 lambda 表达式时,必须使用 template-parameter-list 语法来指定模板参数列表,并使用尖括号将其包裹起来。同时,由于 lambda 表达式不会被执行,因此我们必须立即调用它并将参数传递给它,以确保其返回类型可以被推导出来。

10. Three-way comparison operator

在 C++20 中,引入了三向比较运算符(three-way comparison operator)<=> 的概念,用于比较两个值的大小。三向比较运算符可以返回三种不同的结果:小于、等于或大于。
使用三向比较运算符,我们可以更简洁地进行对象之间的比较,例如:

class MyClass {
public:
bool operator==(const MyClass& other) const = default;
auto operator<=>(const MyClass&) const = default;
};

MyClass a, b;
if (a < b) {
    // ...
} else if (a > b) {
    // ...
} else {
    // ...
}

在上面的代码中,我们定义了一个名为 MyClass 的类,并实现了 operator== 和 <=> 运算符。然后,我们可以使用 <=> 来比较 a 和 b 之间的大小关系,如果 a 小于 b,则执行第一个分支,如果 a 大于 b,则执行第二个分支,否则执行第三个分支。
需要注意的是,实现 <=> 运算符时,必须保证其具有一定的特性,例如可传递性、对称性等。为了方便起见,C++20 还提供了默认实现的方式,即将 <=> 声明为 default,可以自动生成默认的实现。
此外,对于自定义类型,我们还可以使用 std::strong_ordering 和 std::weak_ordering 等强度更高的比较类型来进一步约束运算符的行为,提高代码的可读性和可维护性。

11. Simplifying implicit lambda capture

在 C++20 中,引入了一个 Defect Report(DR)中提出的新特性,用于简化 lambda 表达式的隐式捕获(implicit capture)。这个特性使得我们可以更方便地对变量进行隐式捕获,不再需要使用冗长的捕获列表。
例如,在 C++11 中,如果我们想要将一个 lambda 表达式中的变量 x 进行隐式捕获,我们需要使用以下语法:

int x = 42;
auto lambda = [=]() { return x; };

在上面的代码中,我们使用 [=] 捕获所有外部变量,并通过 x 来访问其中的一个变量。在 C++20 中,我们可以使用以下语法来进行更简洁的隐式捕获:

int x = 42;
auto lambda = [&x]() { return x; };

在上面的代码中,我们使用 [&x] 将变量 x 进行隐式捕获,并通过 x 来访问其中的一个变量。这样,我们就可以更清晰地表达我们的意图,并避免不必要的捕获。
需要注意的是,该特性只适用于单个变量的情况,如果我们需要捕获多个变量,则仍然需要使用捕获列表来指定它们。此外,该特性目前还没有得到所有编译器的完全支持,因此在使用时需要注意代码的兼容性。

12. init-statements for range-based for

C++20引入了在range-based for循环中使用初始化语句(init-statements)的功能。在C++20之前,range-based for循环只允许一个范围表达式,并且要在循环之外单独声明循环变量。而在C++20中,我们可以在range-based for循环中使用初始化语句。
使用初始化语句,我们可以在range-based for循环的头部声明并初始化循环变量。这样可以方便地限定循环变量的作用域,并且可以在循环的每次迭代中重新初始化该变量。例如:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers{1, 2, 3, 4, 5};

    for (int i = 0; auto num : numbers) {
        std::cout << "Iteration: " << i++ << ", Number: " << num << std::endl;
    }

    return 0;
}

上面的代码中,我们使用初始化语句int i = 0在range-based for循环的头部声明并初始化了循环变量i。在每次循环迭代时,都会打印出当前迭代次数和对应的元素值。
这种使用初始化语句的方式提供了更便利的循环控制和变量作用域管理的方式,使得range-based for循环更加灵活和强大。

13. Default constructible and assignable stateless lambdas

在C++中,无状态(stateless)的Lambda函数是指没有捕获任何外部变量的Lambda函数。默认可构造和可赋值的无状态Lambda函数是指这种Lambda函数对象可以通过默认构造函数创建,并且可以使用赋值运算符进行赋值操作。
从C++14开始,无状态Lambda函数可以被默认构造和赋值。这意味着您可以声明一个无参数的无状态Lambda函数,并使用默认构造函数进行初始化,或者将其赋值给其他相同类型的无状态Lambda函数对象。
以下是一个示例:

#include <iostream>

int main() {
    auto lambda1 = []() {
        std::cout << "Hello, world!" << std::endl;
    };

    auto lambda2 = lambda1;  // 使用赋值运算符将lambda1赋值给lambda2

    lambda1();  // 调用lambda1
    lambda2();  // 调用lambda2

    return 0;
}

在上面的示例中,我们声明了一个无状态Lambda函数lambda1,它不捕获任何外部变量并打印出一条消息。然后,我们通过赋值运算符将lambda1赋值给lambda2,并分别调用它们来验证它们可以正常工作。
请注意,如果一个Lambda函数有捕获的外部变量,或者如果它包含有状态(带有成员变量或重载的函数调用运算符),则将不能使用默认构造函数和赋值运算符进行构造和赋值。只有无状态的Lambda函数才可以默认构造和赋值。

14. const mismatch with defaulted copy constructor

在C++中,默认的拷贝构造函数会使用成员逐个拷贝的方式来复制对象的状态。当在类中存在 const 成员变量时,它们不能进行非 const 拷贝,并且默认的拷贝构造函数也无法满足这种情况。
例如,考虑下面的代码:

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass& other) = default;

private:
    const int value = 42;
};

int main() {
    MyClass obj1;
    MyClass obj2(obj1);  // 错误:尝试进行非 const 拷贝
    return 0;
}

上述代码中,MyClass 类包含一个 const 成员变量 value,并使用默认的拷贝构造函数。在 main() 函数中,我们尝试用 obj1 初始化 obj2,但是默认的拷贝构造函数无法复制 const 成员变量,因此会导致编译错误。
要解决这个问题,可以自定义拷贝构造函数并使用初始化列表来处理 const 成员变量的拷贝。例如:

class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass& other) : value(other.value) {}  // 自定义拷贝构造函数

private:
    const int value = 42;
};

int main() {
    MyClass obj1;
    MyClass obj2(obj1);  // 正确:调用自定义的拷贝构造函数
    return 0;
}

在上述代码中,我们提供了自定义的拷贝构造函数,并使用初始化列表将 other.value 初始化为新对象的 value 成员变量。这样就可以正确地复制含有 const 成员变量的对象。
需要注意的是,在定义自定义拷贝构造函数时,确保对其他非 const 成员变量也进行正确的拷贝操作,以保持对象状态的一致性。

15. Access checking on specializations

特化的访问检查是指对C++模板特化的访问权限进行检查的机制。在C++中,模板特化是一种为特定类型或条件提供专门定义的方式。然而,这些特化可能受到与原始模板不同的访问级别约束。
当通过模板特化提供私有(private)或受保护(protected)成员时,编译器将执行特化的访问检查。这意味着对于这些特化,只有在可以访问相应类或模板的作用域内,才能实例化并使用该特化。
下面是一个示例来说明特化的访问检查:

template <typename T>
struct MyTemplate {
    void DoSomething() {     // 普通成员函数
        std::cout << "Do something" << std::endl;
    }
};

// 以私有方式特化 MyTemplate
template <>
struct MyTemplate<int> {
private:
    void DoSomethingSpecialized() {   // 私有成员函数
        std::cout << "Do something specialized" << std::endl;
    }

public:
    void DoSomething() {     // 公有成员函数
        DoSomethingSpecialized();
    }
};

int main() {
    MyTemplate<float> obj1;
    obj1.DoSomething();   // 可以访问普通版本的 DoSomething()

    MyTemplate<int> obj2;
    obj2.DoSomething();   // 错误:无法访问私有成员函数 DoSomethingSpecialized()

    return 0;
}

在上面的示例中,我们有一个模板MyTemplate,它拥有一个普通的成员函数DoSomething()。然后,我们特化了该模板,以针对int类型提供了一个私有的成员函数 DoSomethingSpecialized() 并在公有成员函数DoSomething()中使用它。
在main()函数中,我们实例化了两个对象obj1和obj2,分别使用了普通版本和特化版本的DoSomething()函数。由于特化版本的私有成员函数的访问性,尝试访问obj2.DoSomething()会导致编译错误。
因此,特化的访问检查确保只能在具有适当访问权限的上下文中使用特化版本,从而提供了更严格的访问控制。

16. ADL and function templates that are not visible

ADL(Argument-Dependent Lookup)是C++中的一个机制,用于查找与函数参数相关的名称。当在函数调用中使用未声明的函数名时,编译器会在调用点所属的命名空间和参数类型的关联命名空间中进行搜寻相关的函数。
然而,在涉及到函数模板的情况下,如果函数模板本身不可见(即不在当前作用域内),则ADL无法生效。换句话说,对于不可见的函数模板,即使其模板参数类型匹配,编译器也不会扩展ADL来查找其关联的函数。取而代之的是,它将继续查找已经在作用域内可见的函数或函数模板。
这里有一个示例来说明这个问题:

namespace A {
    struct Foo {};

    // 不可见的函数模板
    template <typename T>
    void func(T) {}
}

namespace B {
    void func(A::Foo) { std::cout << "B::func" << std::endl; }
}

int main() {
    A::Foo foo;
    func(foo);  // 错误:无法找到 A::func 的匹配项

    return 0;
}

在上面的示例中,我们定义了一个名为A::Foo的结构体,并在namespace B中定义了一个函数func,接受A::Foo作为参数。注意,在namespace B中定义的func函数与A命名空间中的不可见函数模板func在函数参数类型上是匹配的。
在主函数中,我们尝试调用func(foo),期望ADL可以找到namespace A中的函数模板func。然而,由于函数模板func本身在当前作用域(主函数)中是不可见的,导致ADL无法生效,编译器将无法找到与A::Foo关联的函数。
因此,对于不可见的函数模板,应确保进行适当的声明或提供必要的前向声明,以便让ADL能够找到相关的函数模板定义。

17. Specify when constexpr function definitions are needed for constant evaluation

需要对constexpr函数进行定义的情况是在你希望该函数在编译时求值并产生一个constexpr结果,以便在其他constexpr上下文中使用。
在C++中,constexpr函数是指可以在编译时求值的函数,如果其参数是常量表达式。可以将函数声明为constexpr,但为了允许函数在编译时求值,其定义必须满足一定的要求。
constexpr函数的定义应该由一个单独的带有constexpr表达式的返回语句组成,并且不应包含任何副作用。这使得编译器可以在编译时求值该函数,并在编译时使用计算出的值进行性能优化或编译时计算。
下面是一个示例来说明这一点:

constexpr int Square(int x) {
    return x * x;
}

int main() {
    constexpr int result = Square(5);  // 在编译时求值

    int n;
    std::cin >> n;
    int dynamicResult = Square(n);     // 在运行时求值

    return 0;
}

在上面的代码中,Square函数被声明为constexpr,并且其定义由一个带有constexpr表达式的返回语句组成。在main函数中,当调用Square(5)时,由于参数是常量表达式,结果可以在编译时计算出。然而,当使用用户提供的值n调用Square(n)时,计算会在运行时进行,因为参数直到运行时才能确定。
因此,在希望进行编译时求值并在constexpr上下文中使用结果的情况下,请确保定义constexpr函数以满足编译时求值的要求。

18. Attributes [[likely]] and [[unlikely]]

[[likely]]和[[unlikely]]是C++17引入的属性(attributes),用于提供对条件分支的优化提示。
这些属性用于向编译器提供关于分支的概率信息,以帮助它在生成目标代码时进行优化。使用[[likely]]属性可以提示编译器某个条件的分支很可能会经常执行,而[[unlikely]]则提示编译器某个条件的分支很可能很少执行。
这些属性只是一种建议,并不保证编译器会按照提示进行优化。编译器可以使用这些提示来调整生成的代码,例如对于概率高的分支路径,它可以尝试将其放在跳转指令的目标位置附近,从而减少分支预测错误。
下面是一个示例,演示了如何使用[[likely]]和[[unlikely]]属性:

#include <iostream>

bool ProcessData(int data) {
    if (data < 0) {
        [[unlikely]]  // 分支概率低
        {
            std::cout << "Less than zero" << std::endl;
            return false;
        }
    } else {
        [[likely]]    // 分支概率高
        {
            std::cout << "Greater than or equal to zero" << std::endl;
            return true;
        }
    }
}

int main() {
    ProcessData(10);
    ProcessData(-5);
    return 0;
}

在上述示例中,我们根据data参数的值进行条件分支。对于data >= 0的情况,我们使用[[likely]]属性提示编译器该分支概率高;而对于data < 0的情况,我们使用[[unlikely]]属性提示编译器该分支概率低。
请注意,这些属性的效果依赖于特定的编译器和目标体系结构,不同的编译器可能对这些属性有不同的实现方式和行为。因此,在使用时请仔细阅读编译器的文档,并进行性能测试和分析,以确定是否产生了预期的优化效果。

19. Make typename more optional

从C++17开始,模板代码中的typename关键字更加灵活,可以在某些情况下省略使用。在此之前,它在声明依赖类型时是必需的,但现在在特定上下文中可以省略typename关键字。
以下是可以省略typename关键字的主要情况:

  1. 在模板中引用非依赖基类名称或枚举器时:
template <typename T>
struct MyStruct : T {
    void func() {
        baseFunction(); // 在访问继承成员时不需要"typename"
    }
};
  1. 在返回类型后置语法中:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}
  1. 使用作用域解析运算符(::)访问嵌套类型或成员时:
template <typename T>
void MyFunction() {
    typename T::NestedType nestedObj; // 此处"typename"是可选的
    nestedObj.memberFunction();
}
  1. 在范围-based for 循环中:
template <typename Container>
void PrintElements(const Container& container) {
    for (auto it = container.begin(); it != container.end(); ++it) {
        std::cout << *it << ' ';
    }
}

尽管在某些情况下可以省略typename关键字提供了一些方便,但请注意,在某些情况下仍然需要使用它。例如,在引用依赖类型名称或声明模板类型参数时,仍然需要使用typename关键字。
请记住,即使在某些情况下可以省略typename,明确使用它也有助于提高代码的可读性,并使意图更加清晰。

20. Pack-expansions in lambda init-captures

在C++中,自C++20起支持使用展开包(pack-expansion)在lambda的init-capture中进行参数初始化。
Lambda表达式的init-capture允许我们在创建lambda函数时,通过初始化器为其捕获的变量进行赋值。而在C++20之前,无法直接使用展开包在init-capture中进行参数初始化。
下面是一个示例,演示了如何在lambda的init-capture中使用展开包:

#include <iostream>

template<typename... Args>
auto createLambda(Args&&... args) {
    return [...args = std::forward<Args>(args)]() {
        ((std::cout << args << ' '), ..., (std::cout << '\n'));
    };
}

int main() {
    auto lambda = createLambda("Hello", "World", 42);
    lambda();

    return 0;
}

在上述示例中,我们定义了一个createLambda函数模板,它接受可变数量的参数,并返回一个lambda函数。在lambda的init-capture中,我们使用展开包…args将传入的参数进行捕获,并使用= std::forward(args)进行初始化。
然后,在lambda函数的主体中,我们使用展开包(std::cout << args << ’ ')来打印每个捕获参数的值,并使用(std::cout << ‘\n’)打印换行符。
运行此程序会输出: “Hello World 42”。
这样,我们就可以在lambda的init-capture中使用展开包对捕获的参数进行初始化,从而更灵活地操作和使用lambda函数。需要注意的是,使用展开包时,确保捕获的变量是可复制或可移动的。同时,请记住在编译器中启用C++20特性。

21. Attribute [[no_unique_address]]

[[no_unique_address]]是C++20引入的属性(attribute),用于对空成员进行优化的提示。
在C++中,类的数据成员占用内存空间。当有多个空成员存在时,每个成员都需要占用至少一个字节的内存空间,这可能导致内存浪费。然而,某些空成员对于对象的唯一内存布局并不重要,因此可以通过[[no_unique_address]]属性对其进行优化。
该属性可应用于非静态数据成员,并告诉编译器,如果成员没有地址依赖性,可以共享存储空间,从而减小对象的尺寸。
下面是一个示例,演示了如何使用[[no_unique_address]]属性:

#include <iostream>

struct EmptyStruct {};

struct MyStruct {
    int value;
    [[no_unique_address]] EmptyStruct emptyMember;

    MyStruct(int v) : value(v) {}
};

int main() {
    std::cout << sizeof(MyStruct) << '\n';  // 输出 sizeof(MyStruct) 的大小

    return 0;
}

在上述示例中,MyStruct结构体包含一个整数成员value和一个类型为空的成员emptyMember。我们使用[[no_unique_address]]属性将emptyMember标记为没有唯一地址依赖性。
通过运行程序,你会发现MyStruct的大小并不受emptyMember的存在影响。由于emptyMember被标记为[[no_unique_address]],它与value共享存储空间,从而减小了MyStruct的尺寸。
请注意,[[no_unique_address]]属性只能应用于空成员或者成员类型的所有非静态数据成员都是有相同地址可行的情况。在使用该属性时,请确保正确理解类的内存布局和对底层对象的操作。
这种优化通常适用于使用标记位、状态标志等的辅助成员变量,以减小对象的内存消耗。最佳实践是在适当情况下使用[[no_unique_address]]属性,并通过测试和性能分析,确保达到预期的优化效果。

22. Conditionally Trivial Special Member Functions

在C++20之后,可以通过条件编译的方式标记特殊成员函数(Special Member Functions)为条件可平凡(conditionally trivial),即根据某些条件确定特殊成员函数是否为平凡的。
在条件可平凡特殊成员函数被声明为平凡时,编译器可以执行更多的优化,例如进行结构布局的优化和零初始化的优化。这对于具有特定需求的性能敏感代码可能是有益的。
下面是一个示例,展示了如何使用宏定义和std::is_trivially_constructible类型特性来条件地标记特殊成员函数为条件可平凡:

#include <iostream>
#include <type_traits>

#define CONDITIONAL_TRIVIAL(clazz) \
    (std::is_trivially_constructible<clazz>::value ? clazz() {}

class MyClass {
public:
    CONDITIONAL_TRIVIAL(MyClass);

    // Other member functions and data members...
};

int main() {
    MyClass obj;
    std::cout << "Object created." << std::endl;

    return 0;
}

在上述示例中,我们使用宏定义CONDITIONAL_TRIVIAL来条件地定义默认构造函数。
std::is_trivially_constructible::value是一个类型特性,在编译时检查给定类是否是平凡可构造的。如果是,则使用大括号内的语句创建默认构造函数,达到将其标记为条件可平凡的效果。
这样,如果类MyClass是平凡可构造的,将使用大括号内定义的默认构造函数。否则,将编译器生成的默认构造函数。
注意,在使用条件可平凡特殊成员函数时,需要确保对应的条件和标记是正确的,并进行充分的测试和验证以确保达到预期的行为。
请注意,以上示例中的宏定义只适用于默认构造函数。如果需要使用其他特殊成员函数(如复制构造函数、移动构造函数、析构函数等),类似的宏定义和类型特性检查可以进行相应的扩展。
最佳实践是在必要的情况下谨慎使用条件可平凡特殊成员函数,并针对具体需求进行性能测试和分析,以确定是否产生了预期的优化效果。

23. Relaxing the range-for loop customization point finding rules

在C++20中,确实放宽了范围for循环的定制点查找规则,以增强其可扩展性和灵活性。
在此之前,用户自定义类型要想支持范围for循环,需要为该类型提供名为begin()和end()的成员函数来返回迭代器。然而,这种限制对于某些类可能不够灵活,特别是当类无法直接提供begin()和end()成员函数的情况下。
在C++20中,如果用户自定义类型未提供begin()和end()成员函数,编译器将会尝试以下寻找定制点的规则:

  1. 首先,编译器将查找非成员函数ranges::begin和ranges::end,通过使用ADL(关联命名空间搜索)在符合约束的命名空间中查找。
  2. 如果第一步没有找到匹配的begin和end函数,则编译器将尝试使用非成员函数std::begin和std::end,这些函数位于全局命名空间中。
  3. 最后,如果以上两个步骤都没有找到适合的函数,则编译器将回退到传统的基于成员函数的查找方式,即寻找类的成员函数begin()和end()。
    通过这种放宽的定制点查找规则,C++20中的范围for循环更加灵活,用户可以在不修改目标类型代码的情况下,对其进行范围迭代。
    请注意,放宽的范围for循环定制点查找规则仅适用于C++20及以后的版本。在使用旧版的C++标准时,仍需要自定义begin()和end()成员函数来支持范围for循环。

24. Allow structured bindings to accessible members

C++20允许对可访问成员使用结构化绑定。
在C++20中,结构化绑定语法得到了扩展,现在可以将其应用于类的可访问成员。结构化绑定是一种通过单个声明将多个变量绑定到一个复合类型(如结构体或类)的成员上的便捷方式。
在之前的C++版本中,结构化绑定只能用于解构元组、数组等已存在的复合类型。但在C++20中,我们可以使用结构化绑定来直接访问类的成员,而无需通过getter函数或公共数据成员来访问。
这种新的语法使得代码更加简洁和可读。通过结构化绑定,我们可以一次性地将类的多个成员绑定到不同的变量中,而不需要分别访问每个成员。
下面是一个使用结构化绑定访问可访问成员的示例:

#include <iostream>

class MyClass {
public:
    int member1;
    float member2;
};

int main() {
    MyClass obj{42, 3.14};

    auto [var1, var2] = obj; // 结构化绑定

    std::cout << "member1: " << var1 << std::endl;
    std::cout << "member2: " << var2 << std::endl;

    return 0;
}

上述代码中,我们定义了一个名为MyClass的类,其中包含两个可访问成员member1和member2。在main函数中,我们创建了一个MyClass对象obj并初始化其成员。然后,通过结构化绑定语法,我们将obj的成员分别绑定到了名为var1和var2的变量上。最后,我们打印出这两个变量的值。
使用C++20允许对可访问成员使用结构化绑定,可以提高代码的可读性和简洁性,并且减少了访问类成员的冗余代码。

25. Destroying operator delete

在C++20中,引入了一种新的析构删除(Destroying operator delete)机制,它可以与全局的分配函数(operator new、operator new[])和释放函数(operator delete、operator delete[])相关联。
这项改变旨在解决使用全局分配和释放函数时的一些问题,特别是当类的分配和释放操作需要进行特殊处理时。以前,在编写自定义的分配和释放函数时,可能需要重载全局的operator new/delete函数,或者使用placement new/delete来实现自定义内存管理。而新的析构删除机制则提供了更灵活的方式。
通过在类中声明和定义一个特定的静态析构删除函数,可以覆盖全局的operator delete函数。这样,在对象销毁时,会调用该类的析构删除函数来执行特定的释放操作。相比于全局的operator delete函数,析构删除函数能够按照类的粒度进行内存管理,并且能够方便地使用类的成员变量和方法。
下面是一个示例代码,展示了如何在C++20中使用析构删除:

#include <iostream>

class MyClass {
public:
    static void operator delete(void* ptr, std::size_t size) noexcept {
        std::cout << "Custom delete called: " << size << " bytes\n";
        // 自定义的释放操作
        ::operator delete(ptr);
    }
    
    void* operator new(std::size_t size) {
        std::cout << "Custom new called: " << size << " bytes\n";
        // 自定义的分配操作
        return ::operator new(size);
    }
    
    void* operator new(std::size_t size, const std::nothrow_t&) noexcept {
        std::cout << "Custom nothrow new called: " << size << " bytes\n";
        // 自定义的无异常分配操作
        return ::operator new(size, std::nothrow);
    }
};

int main() {
    MyClass* obj = new MyClass();
    delete obj;
    
    return 0;
}

在上面的代码中,我们通过在MyClass类中声明和定义了自定义的operator delete函数,覆盖了全局的operator delete函数。当对象销毁时,会调用这个自定义的析构删除函数来执行特定的释放操作。
综上所述,C++20中的析构删除机制提供了更灵活和可控的内存管理方式,使得我们可以更精确地控制对象的分配和释放过程,并且能够方便地使用类的成员变量和方法来完成自定义的操作。

26. Class types in Non-type template parameters

C++20引入了一项重要的功能,允许在非类型模板参数中使用类类型(Class types)。在此之前,非类型模板参数只能是整数、枚举类型或指针类型。现在,我们可以使用类类型作为非类型模板参数,这使得模板更加灵活和通用。
使用类类型作为非类型模板参数可以带来许多有用的特性。首先,我们可以将对象作为模板参数进行传递,而不仅仅限于指针或引用。这样可以让我们在编译时期就进行严格的类型检查,而不必依赖于运行时。
其次,类类型作为非类型模板参数还使得模板可以根据对象的不同进行特化。通过提供不同的实例化模板,我们可以根据不同的类类型参数生成不同的代码,从而提高代码的复用性和效率。
示例代码如下:

template <typename T, T value>
class MyClass {
public:
    void printValue() {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    MyClass<int, 42> obj1;
    obj1.printValue(); // 输出:Value: 42

    MyClass<std::string, "Hello"> obj2;
    obj2.printValue(); // 输出:Value: Hello

    return 0;
}

在上面的示例中,MyClass 是一个接受类型模板参数 T 和非类型模板参数 value 的模板类。我们通过实例化 MyClass 并传递不同的类型和值来创建对象 obj1 和 obj2,然后调用 printValue 函数打印出对应的值。
需要注意的是,在使用类类型作为非类型模板参数时,必须保证该类类型是完整的、可求值的,并且可以在编译期间进行常量表达式求值。另外,类类型的默认构造函数必须是 constexpr 的,以便在编译时进行初始化。
总结而言,C++20中引入的非类型模板参数支持类类型的特性使得模板更加灵活和强大,可以用于更多的应用场景,从而提高代码的复用性和效率。

27. Deprecate implicit capture of this via [=]

在C++20中,可以通过使用[=]来捕获this指针的隐式捕获进行废弃。
隐式捕获是一种在Lambda表达式中自动捕获变量的机制。在旧版本的C++中,如果在Lambda表达式中没有显式地指定捕获列表,那么默认情况下会进行隐式捕获,其中包括对this指针的隐式捕获。这意味着Lambda函数可以访问其所在类的成员变量和成员函数。
然而,在C++20中,已经废弃了隐式捕获this指针的行为。现在,如果想要在Lambda函数中使用this指针,必须显式指定捕获列表并将this包含在其中。例如,可以使用以下方式来显式捕获this:

auto lambda = [this]() {
    // 在Lambda函数中使用this指针
};

使用explicit this捕获方式可以增加代码的可读性,并且使代码更加清晰明确。这样做可以避免不必要的错误和歧义,提高代码质量和可维护性。
因此,建议在C++20中避免使用隐式捕获的方式来捕获this指针,而是显式地使用[=]或[&]捕获列表,并将this包含在其中。

28. Integrating feature-test macros

在C++20中,引入了一些新的特性测试宏,可以用于在编译时检查某个功能是否可用。这些特性测试宏使用预定义的宏来表示不同的功能,并且可以通过判断这些宏是否被定义来确定编译器是否支持相应的功能。
以下是一些常用的特性测试宏:
● __cpp_modules:用于测试模块化编程是否可用。
● __cpp_concepts:用于测试概念(Concepts)是否可用。
● __cpp_coroutines:用于测试协程(Coroutines)是否可用。
● __cpp_ranges:用于测试范围(Ranges)是否可用。
● __cpp_constexpr_dynamic_alloc:用于测试动态分配的constexpr是否可用。
可以通过在代码中使用条件编译来检查特定的特性是否可用,例如:

#ifdef __cpp_modules
    // 模块化编程可用
#endif

#ifdef __cpp_concepts
    // 概念可用
#endif

#ifdef __cpp_coroutines
    // 协程可用
#endif

#ifdef __cpp_ranges
    // 范围可用
#endif

#ifdef __cpp_constexpr_dynamic_alloc
    // 动态分配的constexpr可用
#endif

需要注意的是,特性测试宏的命名规则为__cpp_加上相应特性的名称,所有字母大写,并用下划线分隔单词。它们是由编译器定义的,因此在不同的编译器中可能会有所差异。
这些特性测试宏可以帮助开发者在编译时根据不同的编译环境选择不同的代码路径,以确保代码在不同的编译器和版本中都能正常运行。

29. Prohibit aggregates with user-declared constructors

在C++20中,可以使用"prohibit aggregates with user-declared constructors"的方式禁止具有用户声明构造函数的聚合体。
在C++20之前的标准中,如果一个类满足聚合体的定义,它就不能有任何用户声明的构造函数。然而,在C++20中,这个限制被放宽了,允许聚合体具有用户声明的构造函数。
要禁止具有用户声明构造函数的聚合体,可以使用以下方法:

struct MyAggregate {
    MyAggregate() = default;
    MyAggregate(int x) { /* constructor code */ }
    // other member variables and functions
};

static_assert(!std::is_aggregate_v, “MyAggregate cannot be an aggregate”);
在上面的示例中,我们通过将MyAggregate的一个构造函数声明为用户声明构造函数来阻止它成为聚合体。然后,我们使用std::is_aggregate_v检查类型是否是聚合体,并使用static_assert在编译时产生错误信息。
请注意,禁止聚合体的行为在不同的编译器和标准库实现中可能会有所不同。因此,最好在特定的平台上进行测试以确保预期的行为。

30. constexpr virtual function

C++20引入了对constexpr虚函数的支持。在C++中,constexpr关键字用于指示编译器在编译时计算表达式的值。虚函数是一种动态绑定的机制,允许子类覆盖父类的实现。通过将constexpr和虚函数结合起来,我们可以在编译时确定虚函数的调用结果。
以下是一个示例,展示如何在C++20中使用constexpr虚函数:

#include <iostream>

class Base {
public:
    constexpr virtual int getValue() const {
        return 10;
    }
};

class Derived : public Base {
public:
    constexpr virtual int getValue() const override {
        return 20;
    }
};

int main() {
    constexpr Base* basePtr = new Derived();

    // 在编译时计算虚函数的结果
    constexpr int value = basePtr->getValue();

    std::cout << "Value: " << value << std::endl;

    delete basePtr;

    return 0;
}

在上面的示例中,我们定义了一个基类Base和一个派生类Derived。基类中有一个constexpr虚函数getValue(),而派生类中覆盖了该虚函数,返回不同的值。
在main()函数中,我们创建了一个指向派生类对象的基类指针basePtr。然后,我们使用constexpr修饰符将其声明为constexpr,并调用了getValue()函数。由于constexpr函数在编译时可计算,所以虚函数的调用结果也可以在编译时确定。
最后,我们输出了虚函数的结果,并释放了动态分配的内存。
请注意,constexpr虚函数需要满足一些限制条件:它们的返回类型必须是字面量类型,并且它们的实现必须是constexpr。另外,派生类中覆盖虚函数的实现也必须是constexpr的。

31. Consistency improvements for comparisons

C++20为比较操作引入了一些一致性改进。这些改进的目的是提高比较操作的一致性和可读性。下面是一些主要的改进:

  1. 您可以使用<=>运算符进行三路比较。这个运算符返回一个特殊的std::strong_ordering类型,它表示两个值的关系(相等、小于或大于)。这样的比较运算符在不同类型之间自动推导出来,并且可以用于排序容器中的元素。
  2. 引入了三种新的比较类别:std::strong_orderingstd::weak_orderingstd::partial_ordering。这些类别提供了更精确的比较结果,例如对浮点数的处理更加细致。
  3. 改进了对象类型之间比较的默认行为。现在,当您没有为对象定义比较运算符时,编译器会生成默认的比较运算符。这些默认的比较运算符基于成员变量的逐个比较,从而提供了一致性的默认行为。
    这些改进使得比较操作更加易于使用和理解,同时提供了更丰富的比较选项。这些改进对于开发各种类型的应用程序都非常有用,尤其是需要进行复杂比较的情况。

32. char8_t

在C++20中,引入了一个新的字符类型char8_t,用于表示UTF-8编码的字符。UTF-8是一种广泛使用的Unicode字符编码方案,它可以表示几乎所有的Unicode字符。
char8_t类型主要用于处理和存储UTF-8编码的字符串。与传统的char类型不同,char8_t类型保证一个字节只占用8个比特位,这样可以确保正确地处理和操作UTF-8编码的字符数据。
在C++20中,你可以使用char8_t类型来声明变量、数组或者字符串常量,例如:
char8_t myChar = u8’中’;
char8_t myString[] = u8"这是一个UTF-8编码的字符串";
需要注意的是,在使用char8_t类型时,你需要明确指定u8前缀来表示一个UTF-8编码的字符或字符串常量。这样可以告诉编译器按照UTF-8编码处理相关数据。
同时,C++20也提供了一些新的标准库函数来处理char8_t类型的字符和字符串,例如std::u8string等。
总而言之,C++20中引入的char8_t类型为我们提供了更好的支持和处理UTF-8编码的能力,使得开发者能够更方便地处理多语言字符数据。

33. std::is_constant_evaluated()

std::is_constant_evaluated() 是 C++20 中引入的一个函数模板,用于检查当前代码是否在常量求值上下文中执行。
常量求值(constant evaluation)是 C++20 中的一个新特性,它允许在编译时对一些表达式进行求值。这种求值发生在编译器的常量表达式上下文中,例如 constexpr 函数或 constexpr 变量的初始化过程。
std::is_constant_evaluated() 返回一个 bool 值,如果当前代码在常量求值上下文中执行,则返回 true;否则返回 false。这个函数可以用于编写通用代码,根据代码是在常量求值上下文中还是在运行时上下文中执行来采取不同的行为。
以下是一个示例代码,展示了如何使用 std::is_constant_evaluated():

#include <iostream>
#include <type_traits>

constexpr int add(int x, int y) {
    if (std::is_constant_evaluated()) {
        // 在常量求值上下文中执行的代码
        return x + y;
    } else {
        // 在运行时上下文中执行的代码
        std::cout << "add() called at runtime" << std::endl;
        return x + y;
    }
}

int main() {
    constexpr int result = add(3, 4);
    std::cout << "Result: " << result << std::endl;
    
    int dynamicResult = add(5, 6);
    std::cout << "Dynamic Result: " << dynamicResult << std::endl;
    
    return 0;
}

在上面的示例代码中,add() 函数检查 std::is_constant_evaluated() 的返回值来确定它是在常量求值上下文中还是在运行时上下文中执行。根据执行上下文的不同,函数采取不同的行为。
当使用 add(3, 4) 进行编译时求值时,由于参数都是常量表达式,std::is_constant_evaluated() 返回 true,因此代码块中的内容会被执行,并返回结果 7。
而对于 add(5, 6),它是在运行时调用的,因此 std::is_constant_evaluated() 返回 false,所以代码块中的输出语句会执行,并返回动态计算的结果 11。

34. constexpr try-catch blocks

在C++20中,我们可以在constexpr函数中使用try-catch块。这意味着我们可以在编译时进行异常处理。
通常情况下,constexpr函数被要求在编译时计算结果,并且不能抛出异常。但是,在C++20中,我们可以在constexpr函数的内部使用try-catch块来捕获并处理异常。
然而,需要注意的是,constexpr函数中的异常处理只有在编译时发生的异常才能被捕获。在运行时发生的异常仍然不能捕获。
下面是一个示例代码,演示了如何在C++20中使用constexpr try-catch块:

#include <iostream>

constexpr int divide(int a, int b) {
    try {
        if (b == 0)
            throw "Division by zero";
        else
            return a / b;
    } catch (...) {
        return 0;
    }
}

int main() {
    constexpr int result = divide(10, 2);
    std::cout << "Result: " << result << std::endl;

    constexpr int errorResult = divide(10, 0);
    std::cout << "Error Result: " << errorResult << std::endl;

    return 0;
}

在上面的示例中,divide()函数使用try-catch块来捕获除以零的异常。如果除法操作成功,将返回结果;否则,将返回0。
请注意,divide()函数必须在编译时执行,这意味着它必须被标记为constexpr。通过在main()函数中使用constexpr变量来调用divide()函数,我们可以在编译时获取结果并输出。

35. Immediate functions (consteval)

在C++20中,引入了一种新的函数类型称为consteval函数,它被设计为立即可求值函数。consteval函数是在编译期间执行的,而不是在运行时执行的。
与常规函数不同,consteval函数必须满足以下条件:

  1. 必须是constexpr函数。
  2. 函数体内只能包含constexpr语句。
  3. 函数参数和返回值必须是字面类型(literal type)。
    由于consteval函数在编译期间执行,它们可以用于需要在编译时进行计算或验证的场景。例如,可以使用consteval函数来检查函数模板的类型参数是否满足某些要求,并在编译期间对其进行报错。
    下面是一个使用consteval函数的示例:
consteval int square(int x) {
  return x * x;
}

int main() {
  constexpr int result = square(5);  // 在编译期间计算结果
  static_assert(result == 25);
  
  // 下面的代码将导致编译错误,因为consteval函数不能在运行时调用
  int value = 10;
  int dynamicResult = square(value);  // 错误!不能在运行时调用consteval函数
}

上述示例中,consteval函数square()在编译期间计算并返回传入参数的平方。main()函数中的result变量在编译期间计算得到值为25,并通过静态断言进行了验证。请注意,尝试在运行时调用consteval函数将导致编译错误。
总结来说,consteval函数是C++20引入的一种特殊函数类型,用于在编译期间进行立即求值。它们提供了更多的编译时计算和验证能力,有助于代码的优化和错误检查。

36. Nested inline namespaces

在C++20中,可以使用嵌套的内联命名空间(nested inline namespaces)来对命名空间进行更细粒度的组织和版本控制。
内联命名空间是指在一个命名空间中定义另一个命名空间,并且在使用时无需显式地指定外层命名空间的名称。而嵌套的内联命名空间则是在内联命名空间中再次定义内联命名空间。
下面是一个示例代码:

#include <iostream>

namespace mylib {
    inline namespace v1 {
        void foo() {
            std::cout << "Version 1\n";
        }
    }

    inline namespace v2 {
        void foo() {
            std::cout << "Version 2\n";
        }
    }
}

int main() {
    mylib::foo();  // 输出:Version 2

    using namespace mylib::v1;
    foo();  // 输出:Version 1

    return 0;
}

在上面的示例中,mylib 命名空间包含了 v1 和 v2 两个内联命名空间。当我们调用 mylib::foo() 时,默认情况下会使用最新版本的 foo() 函数,即 v2 中的实现。但如果我们通过 using namespace 或者 using 指令将 v1 命名空间引入当前作用域,那么就可以直接使用 foo() 函数的 v1 版本。
通过使用嵌套的内联命名空间,我们可以更灵活地对代码进行版本管理和组织,而无需修改现有的代码结构。这对于库的演进和向后兼容性非常有用。

37. Yet another approach for constrained declarations

在C++20中,引入了一种新的语法来声明具有约束条件的变量和函数,这种语法被称为"constrained declarations"。它允许程序员在声明中指定对类型参数的约束条件,以限制类型参数的可能取值范围。
这种方法的主要思想是使用requires子句来定义约束条件。在类型参数后面使用requires关键字,然后在大括号内编写对类型参数的约束条件。例如:

template<typename T>
requires std::is_integral_v<T>
void foo(T value) {
    // 函数体
}

这个例子中,我们使用requires子句来约束模板函数foo的类型参数T必须是整数类型。这样,只有传递整数类型的参数才能调用该函数。
此外,还可以在具体的声明中使用requires子句来限制类型参数的取值范围。例如:

template<typename T>
concept Integral = std::is_integral_v<T>;

template<typename T>
void bar(T value) requires Integral<T> {
    // 函数体
}

在这个例子中,我们首先定义了一个概念Integral,用于判断某个类型是否为整数类型。然后,在函数声明中使用requires子句来限制类型参数T必须满足Integral概念。这样,只有传递整数类型的参数才能调用该函数。
通过这种方式,我们可以在声明中明确指定类型参数的约束条件,以提高代码的可读性和类型安全性。这种方法在C++20中被引入,为编写更加模块化和可靠的代码提供了一种便捷的方式。

38. Changing the active member of a union inside constexpr

在C++20中,允许在常量表达式(constexpr)中更改联合体(union)的活动成员(active member)。在以前的C++版本中,这种操作被认为是未定义行为。然而,在C++20中,可以在常量表达式中进行此类操作,但仍有一些限制。
以下是关于在C++20中在常量表达式中更改联合体活动成员的一些限制和示例:

  1. 只能在初始化期间更改活动成员:在常量表达式中,只能在联合体的初始化期间更改活动成员。这意味着只能在声明或定义联合体变量时进行活动成员的更改。
  2. 联合体必须是字面类型(literal type):要在常量表达式中更改联合体的活动成员,该联合体必须是字面类型。这意味着它必须满足一些特定的条件,如没有非静态数据成员,没有虚函数,没有引用类型成员等。
    下面是一个示例,展示了如何在C++20中在常量表达式中更改联合体的活动成员:
#include <iostream>

union MyUnion {
int intValue;
float floatValue;
};

constexpr MyUnion changeActiveMember(bool useInt) {
    MyUnion u;
    if (useInt) {
        u.intValue = 42;
    } else {
        u.floatValue = 3.14f;
    }
    return u;
}

int main() {
    constexpr MyUnion u1 = changeActiveMember(true);
    std::cout << "u1.intValue = " << u1.intValue << std::endl;  // 输出:u1.intValue = 42

    constexpr MyUnion u2 = changeActiveMember(false);
    std::cout << "u2.floatValue = " << u2.floatValue << std::endl;  // 输出:u2.floatValue = 3.14
}

在上述示例中,我们定义了一个联合体MyUnion,它有两个成员:intValue和floatValue。然后,我们在changeActiveMember函数中根据条件更改活动成员,并将修改后的联合体作为常量表达式返回。最后,在main函数中使用这些常量表达式创建并输出联合体变量的值。
请注意,尽管C++20允许在常量表达式中更改联合体的活动成员,但仍需谨慎考虑使用场景和遵守语言规范。确保对联合体的操作在编译期间是可确定的,并且联合体满足字面类型的要求。

39. Coroutines

协程(Coroutines)是C++20引入的重要概念之一,它提供了一种新的编写异步代码的方式。协程允许函数在执行过程中暂停和恢复,使得异步代码更易于理解、编写和维护。
在C++中,协程通过co_await和co_yield关键字来实现。co_await用于等待一个异步操作的完成,而co_yield用于将控制权返回给调用者并传递一个值。
以下是一个简单的使用协程的示例:

#include <iostream>
#include <coroutine>

class generator {
public:
    struct promise_type {
        int current_value;

        auto get_return_object() { return generator{ this }; }
        auto initial_suspend() { return std::suspend_never{}; }
        auto final_suspend() noexcept { return std::suspend_always{}; }
        void unhandled_exception() {}
        void return_void() {}

        auto yield_value(int value) {
            current_value = value;
            return std::suspend_always{};
        }
    };

    generator(promise_type* p) : p(p) {}

    void next() { p->yield_value(p->current_value + 1); }

    bool done() { return p == nullptr; }

    int value() const { return p->current_value; }

private:
    promise_type* p;
};

generator count_up_to(int limit) {
    for (int i = 0; i <= limit; ++i) {
        co_yield i;
    }
}

int main() {
    auto gen = count_up_to(5);
    while (!gen.done()) {
        std::cout << gen.value() << ' ';
        gen.next();
    }
    return 0;
}

在上面的示例中,我们使用generator类来定义一个简单的协程生成器。该生成器可以产生从0到指定限制值的整数序列。通过使用co_yield关键字,我们可以暂停生成器的执行并将当前值返回给调用者。
在main函数中,我们创建了一个count_up_to生成器,并依次打印生成器的值。通过调用next函数,我们可以让生成器继续执行并产生下一个值,直到生成器完成为止。
这是一个简化的协程示例,实际应用中协程可以帮助我们更好地处理异步任务、事件驱动编程等场景,提供了一种更高效且易于理解的编写异步代码的方式。

40. Parenthesized initialization of aggregates

在C++20中,引入了使用括号进行聚合体的初始化的特性。聚合体是一种特殊类型的类或结构体,其成员变量可以通过简单的列表初始化来初始化。
下面是一个示例,展示了如何使用括号初始化聚合体:

struct Point {
    int x;
    int y;
};

int main() {
    // 使用括号初始化聚合体
    Point p = {1, 2};

    return 0;
}

在上述示例中,我们定义了一个Point结构体作为聚合体,它有两个整型成员变量x和y。然后,我们使用括号对聚合体进行初始化,并为x赋值为1,y赋值为2。
这种括号初始化的方式也适用于数组和其他聚合体,例如:

struct Rectangle {
    int width;
    int height;
};

int main() {
    // 使用括号初始化数组
    int numbers[] = {1, 2, 3, 4, 5};

    // 使用括号初始化聚合体数组
    Rectangle rectangles[] = {{1, 2}, {3, 4}, {5, 6}};

    return 0;
}

在上述示例中,我们使用括号初始化了一个整型数组numbers和一个包含多个矩形的聚合体数组rectangles。
使用括号初始化的好处是可以更简洁地初始化聚合体和数组,并且可以确保所有成员都得到正确的初始化。这是C++20引入的一个方便且直观的特性,使代码更易读和维护。

41. Array size deduction in new-expressions

C++20引入了一个新的语言特征,允许new表达式中自动推断数组大小。这是一个设计提案DR11(Designated Initialization for Arrays)的一部分。
以前,new表达式必须明确指定数组大小,如:
int* arr = new int[10];
C++20允许省略数组大小,让编译器自动推断:
int values[] = {1, 2, 3};
int* arr = new int[]{1, 2, 3}; // 自动推断数组大小为3
这种语法将数组大小的推断与初始化值联系在一起。
主要特点:
● 新语法为new int,initializer-list可以是数值列表或其他数组。
● 编译器会根据初始化值的数量自动推断出数组的大小。
● 数组大小必须是常量表达式,不能依赖运行时值。
● 与C中的动态数组长度一致,更易于移植C代码。
● 避免手动计算数组大小,提高代码可读性和可维护性。
● 与C++17中std::initializer_list配合使用更加方便。
所以总体来说,这是一个小而实用的语言扩展,可以让新建数组更简单和类型安全。它消除了显式指定数组大小这个必要步骤,提高了编程效率。

42. Modules

模块(Modules)是C++20引入的一个重要特性,它旨在改善C的编译速度和可维护性。模块提供了一种新的组织和管理代码的方式,以替代传统的头文件包含。
模块使得代码可以被预编译为二进制形式,而不需要每次编译都重新解析和编译头文件。这样可以加快编译速度,并降低构建大型项目时的依赖关系处理开销。
以下是一个简单的示例,展示了如何使用模块:

// 保存为math.cppm

export module math;

export int add(int a, int b) {
    return a + b;
}

export int subtract(int a, int b) {
    return a - b;
}

上述示例中,我们定义了一个名为math的模块。该模块导出了两个函数add和subtract,用于执行加法和减法运算。通过export module math;语句,我们声明该模块的名称为math。
模块的使用方式如下:

import math;

int main() {
    int result = add(5, 3);
    return 0;
}

在上面的示例中,我们使用import math;语句将math模块导入到当前源文件中。然后,我们可以直接调用该模块中导出的函数,如math::add。
通过模块的使用,我们可以避免传统的头文件包含和预处理器宏等机制。模块提供了更加清晰、模块化和可维护的代码组织方式,同时还能提高编译速度和构建性能。
需要注意的是,模块特性的支持程度因编译器而异。在使用模块之前,请确保你的编译器支持C++20的模块特性,并参考相关文档来了解其语法和用法。

43. Stronger Unicode requirements

C++20引入了更强大的Unicode支持,以提高对Unicode字符和字符串的处理能力。这些改进旨在使C更适合处理多语言和国际化应用程序。
以下是C++20中的一些Unicode相关的改进:

  1. 字符串字面量的编码:C++20允许在字符串字面量中使用Unicode编码的字符,例如UTF-8、UTF-16和UTF-32。这样可以直接在源代码中表示Unicode字符,而不需要使用转义序列。
const char* utf8Str = u8"Hello, 世界!";
const wchar_t* utf16Str = u"Hello, 世界!";
const char32_t* utf32Str = U"Hello, 世界!";
  1. Unicode转义序列:C++20引入了新的Unicode转义序列,以便直接在字符串和字符常量中表示Unicode字符。现在可以使用\u后跟4位十六进制数字表示UTF-16字符,或使用\U后跟8位十六进制数字表示UTF-32字符。
char16_t utf16Char = u'\u4e16';
char32_t utf32Char = U'\U00004e16';
  1. 新的Unicode属性和算法:C++20增加了一些Unicode相关的函数和类,如std::isalnum、std::isalpha、std::islower等,用于判断字符的Unicode属性。此外,还引入了一些Unicode算法,如std::is_sorted_until、std::lexicographical_compare_three_way等。
bool isAlphaNumeric = std::isalnum('A');
  1. Unicode正规化:C++20引入了新的字符类型char8_t,用于表示UTF-8编码的字符。这样可以直接在C中处理和操作UTF-8字符串。
const char8_t* utf8Str = u8"Hello, 世界!";

通过这些改进,C++20提供了更强大和便利的Unicode支持,使开发者能够更轻松地处理多语言应用程序和国际化文本。需要注意的是,具体的Unicode支持可能因编译器而异,请确保你的编译器支持并遵循C++20标准的Unicode要求。

44. <=>、!=和==

<=>、!=和是C++20中引入的运算符。
<=>运算符是三路比较运算符(Three-Way Comparison Operator),用于比较两个对象的大小关系。它返回一个std::strong_ordering类型的结果,表示两个对象之间的顺序关系。这个运算符可以用来代替传统的比较运算符(<、>、<=、>=)和相等性运算符(
、!=)。使用<=>运算符可以更直接地比较两个对象,并且可以灵活地应用于用户自定义类型。
#include

struct Point {
int x;
int y;

auto operator<=>(const Point& other) const = default; // 使用默认的三路比较运算符

};

int main() {
Point p1{1, 2};
Point p2{3, 4};

std::strong_ordering result = p1 <=> p2;

if (result == std::strong_ordering::less) {
    // p1 小于 p2
} else if (result == std::strong_ordering::greater) {
    // p1 大于 p2
} else {
    // p1 等于 p2
}

return 0;

}
!=运算符用于判断两个对象是否不相等。它返回一个bool类型的结果,表示两个对象是否不相等。
bool result = (p1 != p2); // 判断 p1 和 p2 是否不相等
==运算符用于判断两个对象是否相等。它也返回一个bool类型的结果,表示两个对象是否相等。
bool result = (p1 == p2); // 判断 p1 和 p2 是否相等
这些运算符的引入使得比较操作更加直观和便捷,并且可用于用户自定义类型。需要注意的是,支持这些运算符的最小要求是使用C++20及以上的编译器。

45. Lambda capture and storage class specifiers of structured bindings

C++20引入了Lambda捕获和结构化绑定的存储类说明符。下面是对这两个特性的解释以及示例代码:

  1. Lambda捕获是指在Lambda表达式中使用外部变量。在C++20之前,Lambda只能以值或引用的方式捕获变量。而C++20引入了新的捕获方式,可以通过存储类说明符来指定捕获变量的存储类别。有三个存储类说明符可用于捕获:&表示按引用捕获,= 表示按值捕获,而&=表示按引用捕获并将其作为非常量。

示例代码如下:

int main() {
    int x = 10;
    int y = 20;

    // 按引用捕获变量x,按值捕获变量y
    auto lambda1 = [&x, y]() {
        // 使用捕获的变量
        std::cout << "x: " << x << ", y: " << y << std::endl;
    };

    // 按值捕获所有变量
    auto lambda2 = [=]() {
        // 使用捕获的变量
        std::cout << "x: " << x << ", y: " << y << std::endl;
    };

    // 按引用捕获变量x,并将其作为非常量
    auto lambda3 = [&, x]() {
        // 修改捕获的变量
        x += 5;
        std::cout << "x: " << x << ", y: " << y << std::endl;
    };

    lambda1(); // 输出:x: 10, y: 20
    lambda2(); // 输出:x: 10, y: 20
    lambda3(); // 输出:x: 15, y: 20

    return 0;
}
  1. 结构化绑定允许将元组或类对象的成员绑定到单独的变量中。在C++20中,可以使用存储类说明符来指定结构化绑定的存储类别。有两个存储类说明符可用于结构化绑定:autoconst auto

示例代码如下:

#include <tuple>

std::tuple<int, float> getValues() {
    return std::make_tuple(42, 3.14f);
}

int main() {
    auto [a, b] = getValues();
    const auto [c, d] = getValues();

    std::cout << "a: " << a << ", b: " << b << std::endl; // 输出:a: 42, b: 3.14
    std::cout << "c: " << c << ", d: " << d << std::endl; // 输出:c: 42, d: 3.14

    return 0;
}

上面的示例代码演示了如何使用结构化绑定和存储类说明符从getValues()函数返回的元组中提取值,并将它们绑定到变量abcd中。注意,对于const auto绑定的变量,不能修改其值。

46. constexpr container operations

C++11 引入了 constexpr,它允许在编译时求值的表达式。对于容器操作,C++11 并没有直接支持 constexpr 版本,不过自那之后的 C++ 标准增加了更多的支持。

在 C++20 中,引入了一些标准库容器的 constexpr 操作。下面是一些常见的示例:

  1. std::arraysize()empty() 函数:

    constexpr std::array<int, 3> arr = {1, 2, 3};
    static_assert(arr.size() == 3); // 编译时断言
    static_assert(!arr.empty()); // 编译时断言
    
  2. std::vectordata() 函数(注意:只有在容器非空且连续存储时才能使用):

    constexpr std::vector<int> vec = {1, 2, 3};
    constexpr const int* ptr = vec.data();
    
  3. std::stringsize()empty() 函数:

    constexpr std::string str = "Hello";
    static_assert(str.size() == 5); // 编译时断言
    static_assert(!str.empty()); // 编译时断言
    

请注意,要求 constexpr 容器操作时,必须确保在编译时已知相关的条件和数据。另外,某些容器操作仅在特定条件下才能用作 constexpr,因此请查阅相关容器的文档以了解更多细节。

47. Deprecating some uses of volatile

在C++20中,对volatile关键字进行了一些改进和限制。尽管volatile仍然可以用于特定的用例,但它已经不再作为多线程同步或优化层面的替代方案。

以下是一些C++20中对volatile使用的限制和建议:

  1. 移除了某些隐式转换:C++20移除了将volatile对象隐式转换为非volatile对象的能力。现在,任何尝试这样的转换都会导致编译错误。

  2. 无法保证原子性:volatile并不能提供原子性保证,因此无法安全地用于多线程同步。如果需要进行线程同步,请使用专门设计的原子类型和互斥量等并发原语。

  3. 使用std::atomic:C++20引入了std::atomic模板,它提供了适用于多线程环境的原子操作。如果需要实现线程间的可见性或顺序保证,请使用std::atomic而不是volatile

  4. 仍适用于特定硬件或I/O访问:volatile仍然适用于特定的硬件访问或输入/输出(I/O) 操作,例如与内存映射设备的交互。在这些情况下,volatile可以确保读取和写入操作不被编译器优化。

总之,在C++20中,volatile的主要用途是与特定硬件或I/O访问相关。对于多线程同步或优化方面,使用std::atomic和其他并发原语可以更好地满足需求。请注意,为了正确使用volatile,仍然需要详细了解其在特定上下文中的行为和限制,并确保在使用时符合所需的语义和安全性。

48. constinit

constinit 是 C++20 中引入的关键字,用于指定对象必须在编译时初始化,并且只能在静态存储区域或线程局部存储区域中使用。所以constinit修饰的变量无法被修改,相比const修饰的变量可以强制转换来修改值,constinit修饰的变量更加安全。

constinit 关键字可以应用于全局变量、静态变量和局部静态变量,以及类的静态数据成员。它确保这些变量在程序启动期间进行静态初始化,而不是动态初始化。静态初始化是在编译时进行的,而动态初始化是在程序运行时进行的。

以下是一个示例代码,演示了 constinit 关键字的使用:

#include <iostream>

// 全局变量
constinit int globalVar = 42;

void func() {
    // 局部静态变量
    static constinit int localVar = 10;
    std::cout << "Local variable: " << localVar << std::endl;
}

int main() {
    std::cout << "Global variable: " << globalVar << std::endl;
    func();

    return 0;
}

在上面的示例中,globalVar 是一个全局变量,使用 constinit 进行标记。因此,在程序启动期间,它将进行静态初始化。

localVar 是一个局部静态变量,在函数 func() 内使用 constinit 进行标记。这意味着该变量也将在程序启动期间进行静态初始化,并且仅在第一次调用 func() 时初始化。

请注意,constinit 关键字的使用可能限制了一些初始化操作,例如对其他运行时数据的依赖或需要动态分配内存的初始化。因此,在使用 constinit 关键字时,请确保对象的初始化是静态的、与其他代码无关的,并且可以在编译时确定。

49. Deprecate comma operator in subscripts

在C++中,逗号运算符(,)可以在多个上下文中使用,包括数组索引的子表达式。然而,从C++20开始,使用逗号运算符来连接数组下标已经被废弃。

逗号运算符允许按顺序评估多个表达式,整个表达式的值为最后一个子表达式的值。在数组下标中,逗号运算符允许指定多个索引。

例如:

int arr[5][3];
int value = arr[i, j];  // 废弃的用法

// 等同于:
int value = arr[j];

在上面的代码中,ij 是用于二维数组 arr 的不同索引。在 ij 之间使用逗号运算符 , 的用法已经不再推荐,被视为废弃的用法。它等同于仅使用 j 作为索引。

废弃警告是为了避免依赖这种用法,因为它可能导致混淆或误解。相反,建议在访问数组元素时为每个维度使用单独的下标。

需要注意的是,尽管逗号运算符在其他上下文中仍然有效且有用,但在数组下标中的使用已被废弃,以确保代码更清晰、更易读。

50. [[nodiscard]]

在C++20中,[[nodiscard]] 属性得到了进一步的增强和扩展。除了应用于函数、类或枚举类型的声明外,[[nodiscard]] 还可以应用于命名空间、成员函数、成员变量等更多的实体上。

使用 [[nodiscard]] 属性可以帮助开发者避免忽略函数返回值或对象的状态,从而减少潜在的错误和资源浪费。编译器会对标记为 [[nodiscard]] 的实体进行检查,并在忽略其返回值时发出警告。

以下是一个示例,展示了在C++20中如何使用 [[nodiscard]] 属性:

[[nodiscard]] int calculateSum(int a, int b) {
    return a + b;
}

struct [[nodiscard]] MyStruct {
    int value;
};

enum class [[nodiscard]] MyEnum {
    Value1,
    Value2
};

namespace [[nodiscard]] MyNamespace {
    // ...
};

int main() {
    calculateSum(3, 4);  // 警告:未使用函数的返回值

    MyStruct s;
    s.value = 10;  // 警告:未使用结构体的对象

    MyEnum e = MyEnum::Value1;  // 警告:未使用枚举类型的值

    return 0;
}

在上述代码中,calculateSum 函数、MyStruct 结构体、MyEnum 枚举类型以及 MyNamespace 命名空间都标记为 [[nodiscard]]。通过使用这个属性,编译器会在相应的实体被忽略时发出警告。

需要注意的是,[[nodiscard]] 属性仅仅是一种编译器提供的指示机制,并不能强制开发者使用函数返回值或对象状态。因此,开发者仍然需要根据具体情况自觉地应用和使用 [[nodiscard]],以提高代码的可读性和健壮性。

51. Trivial default initialization in constexpr functions

在C++20中,引入了对常量表达式函数的改进,其中包括对于可平凡默认初始化的支持。

可平凡默认初始化是指在常量表达式函数中的变量未显式初始化的情况下,其值自动被设定为零或空。这种特性允许在常量表达式函数中使用未初始化的局部变量,并将其视为已初始化为零或空。

以下是一个示例,演示了在C++20中对于可平凡默认初始化的支持:

constexpr int sum(int a, int b) {
    int result;  // 可平凡默认初始化

    if (a > 0 && b > 0) {
        result = a + b;
    } else {
        result = 0;
    }

    return result;
}

int main() {
    constexpr int result = sum(3, 4);
    // 在C++20之前,上述代码将导致编译错误,因为result未能显式初始化。
    // 但在C++20中,由于result具有可平凡默认初始化,它将被视为已初始化为零。

    return 0;
}

在上述代码中,sum 函数是一个常量表达式函数,在函数内部声明的 result 变量没有显式初始化。在C++20之前,这种情况会导致编译错误。但是在C++20中,由于 result 具有可平凡默认初始化,它被视为已初始化为零。因此,上述代码在C++20中是合法的。

这个特性使得常量表达式函数更加灵活和易用,允许使用未初始化的变量,并将其默认值设定为零或空。

需要注意的是,可平凡默认初始化仅适用于常量表达式函数中的局部变量,并且仅当编译器能够确定变量将始终被初始化为零或空时才成立。如果变量的值依赖于函数参数或其他非常量表达式,则不能享受可平凡默认初始化的好处。因此,还需要根据具体情况谨慎使用并理解这个特性的限制。

52. Unevaluated asm-declaration in constexpr functions

在C++20中,引入了对于常量表达式函数中未评估的 asm-declaration 的支持。

asm-declaration 允许嵌入汇编代码到 C++ 程序中,用于实现一些特定的底层操作。在之前的 C++ 标准中,asm-declaration 在常量表达式函数中是被禁止的,因为它们代表着无法被编译器静态分析和优化的部分。

然而,在 C++20 中,通过允许未评估的 asm-declaration 出现在常量表达式函数中,可以使得某些情况下的底层代码优化成为可能。这意味着在常量表达式函数中可以使用 asm-declaration 来执行一些特定的汇编操作,而不会破坏该函数的常量表达式属性。

以下是一个示例,展示了在 C++20 中允许在常量表达式函数中使用未评估的 asm-declaration:

constexpr int add(int a, int b) {
    int result;

    asm("add %1, %0"
        : "=r" (result)
        : "r" (a), "r" (b)
    );

    return result;
}

int main() {
    constexpr int sum = add(3, 4);
    // 在C++20之前,上述代码将导致编译错误,因为asm-declaration是禁止在常量表达式函数中的。
    // 但在C++20中,允许未评估的asm-declaration出现在常量表达式函数中,使得上述代码在C++20中是合法的。

    return 0;
}

在上述示例中,add 函数是一个常量表达式函数,并使用 asm-declaration 执行了相加操作。在C++20之前,这种情况会导致编译错误。但在C++20中,允许在常量表达式函数中使用未评估的 asm-declaration,因此上述代码是合法的。

需要注意的是,asm-declaration 的使用仍然需要遵循特定平台和编译器的规则和限制。这包括正确处理寄存器约束、内联汇编代码的语法等。因此,在使用 asm-declaration 时,仍然需要小心谨慎,并且了解目标平台和编译器的要求。

总而言之,C++20 允许在常量表达式函数中使用未评估的 asm-declaration,从而为某些底层操作提供更大的灵活性和优化的可能性。但在使用时需谨慎,并遵循特定平台和编译器的规则。

53. using enum

在C++20中,引入了 using enum 语法,用于引入枚举类型的枚举值到当前作用域。这个语法可以使得使用枚举值更加方便和简洁。

使用 using enum 可以将一个枚举类型的枚举值引入到当前作用域,从而可以直接使用枚举值而无需指定其完整的枚举类型名称。

以下是一个示例,展示了如何使用 using enum

enum class MyEnum {
    Value1,
    Value2,
    Value3
};

void foo(MyEnum value) {
    // 使用 using enum 引入枚举值
    using enum MyEnum;

    if (value == Value1) {
        // ...
    } else if (value == Value2) {
        // ...
    } else if (value == Value3) {
        // ...
    }
}

int main() {
    foo(MyEnum::Value1);

    return 0;
}

在上述代码中,定义了一个枚举类型 MyEnum,包含三个枚举值:Value1Value2Value3。在 foo 函数内部,通过使用 using enum MyEnum;,我们将枚举值引入了当前作用域中。这样,在函数内部就可以直接使用枚举值而无需指定其完整的枚举类型名称。

使用 using enum 可以提高代码的可读性和简洁性,特别是在处理具有较长枚举类型名称的代码时。然而,需要注意确保引入的枚举值不会与当前作用域中的其他名称冲突。

需要注意的是,using enum 是 C++20 的新特性,因此在使用时需要确保编译器支持并正确地使用适当的编译选项启用 C++20 特性。

54. Synthesizing Three-way comparison for specified comparison category

在C++20中,引入了一种新的语法来合成指定比较类别(specified comparison category)的三路比较(three-way comparison)。这个特性可以使得自定义类型的对象能够进行自然的比较操作,而无需显式实现所有比较运算符。

通过使用 = default 语法,并结合 std::strong_orderingstd::weak_orderingstd::partial_ordering 等比较类别,可以自动合成具有指定比较类别的三路比较操作。这样,在使用 ==!=<><=>= 操作符时,编译器会根据指定的比较类别自动生成相应的比较函数。

以下是一个示例,展示了如何在C++20中使用指定比较类别来合成三路比较:

#include <compare>

class MyType {
public:
    int value;

    // 合成指定比较类别的三路比较操作
    auto operator<=>(const MyType&) const = default;
};

int main() {
    MyType obj1{10};
    MyType obj2{20};

    if (obj1 == obj2) {
        // ...
    } else if (obj1 < obj2) {
        // ...
    } else if (obj1 > obj2) {
        // ...
    }

    return 0;
}

在上述代码中,定义了一个自定义类型 MyType,其中包含一个整数成员变量 value。通过在类中声明 operator<=> 并使用 = default 来合成指定比较类别的三路比较操作。这样,我们可以直接使用 ==<> 等比较运算符来比较两个 MyType 对象。

需要注意的是,通过合成三路比较操作,编译器会根据对象的成员进行比较,并根据指定的比较类别自动生成适当的返回值。如果希望自定义比较方式,则需要显式实现相关的比较运算符。

总而言之,C++20 中引入了一种新的语法来合成指定比较类别的三路比较,使得自定义类型的对象能够进行自然的比较操作。这种特性提供了更简洁和灵活的比较操作方式,并提高了代码的可读性和可维护性。

55. [[nodiscard]] for constructors

“[[nodiscard]]” 属性是一个 C++17 引入的特性,用于标记函数的返回值应该被检查并且不应该被忽略。然而,在 C++17 中,这个属性只能用于函数的返回类型为非 void 的情况。

在 C++20 之前,对于构造函数而言并没有类似于 “[[nodiscard]]” 这样的属性可以直接应用。但是,可以通过以下方法实现类似的效果:

  1. 使用警告标志:启用编译器的 “-Wunused-result” 或类似的警告标志,以便在构造函数调用的结果未被使用时产生警告。这样可以提醒开发人员检查和处理构造函数的返回值。

  2. 提供明确的命名规范:在构造函数的命名上加入一些约定或前缀,以指示其返回值应该被检查和使用。例如,可以使用 “create”、“new” 或类似的前缀来明确表示这些构造函数的返回值应该被注意。

在 C++20 中,引入了对于构造函数的 “[[nodiscard]]” 属性的支持。这意味着可以直接将 “[[nodiscard]]” 属性应用于构造函数,以告知编译器和其他开发人员构造函数的返回值应该被检查并且不应该被忽略。

以下是一个示例,展示了如何在 C++20 中使用 “[[nodiscard]]” 属性来标记构造函数:

[[nodiscard]] class MyType {
public:
    MyType(int value) : value(value) {}

    // 具有 [[nodiscard]] 属性的构造函数
    [[nodiscard]] static MyType create(int value) {
        return MyType(value);
    }

private:
    int value;
};

int main() {
    // 构造函数返回值未被使用时产生警告
    MyType(10);

    // 使用具有 [[nodiscard]] 属性的构造函数,避免产生警告
    MyType::create(20);

    return 0;
}

在上述代码中,MyType 类被标记为 [[nodiscard]],以提示开发人员不要忽略其构造函数的返回值。在 main 函数中,直接调用构造函数 MyType(10) 会产生警告,因为它的返回值被忽略了;而使用具有 [[nodiscard]] 属性的静态工厂函数 MyType::create(20) 则可以避免警告。

总的来说,C++20 引入了对于构造函数的 [[nodiscard]] 属性的支持,使得可以直接标记构造函数的返回值应该被检查并且不应该被忽略。这提供了一种更加清晰和明确的方式来强制开发人员注意和处理构造函数的返回值。

56. class template argument deduction for aggregates

在 C++17 中引入了对于聚合体(aggregates)的类模板参数推导(class template argument deduction)的支持。这意味着可以通过使用聚合体进行自动的模板参数推导,从而简化代码并使其更具灵活性。

聚合体是一种特殊类型的类,具有以下特征:

  • 所有非静态成员都是公共的;
  • 没有用户声明的构造函数、析构函数和基类;
  • 没有虚函数或虚基类;
  • 没有默认构造函数、拷贝构造函数、移动构造函数、复制赋值运算符或移动赋值运算符。

在 C++17 中,如果使用聚合体作为模板参数进行类模板参数推导,编译器会根据初始化器列表的参数类型来推导出模板参数。

以下是一个示例,展示了在 C++17 中如何使用聚合体进行模板参数推导:

template <typename T>
struct MyPair {
    T first;
    T second;
};

int main() {
    MyPair<int> p1 {10, 20}; // 推导为 MyPair<int>
    MyPair<double> p2 {3.14, 6.28}; // 推导为 MyPair<double>

    return 0;
}

在上述代码中,我们定义了一个类模板 MyPair,它具有两个公共的成员变量 firstsecond。在 main 函数中,我们可以直接使用聚合体的初始化器列表来创建 MyPair<int>MyPair<double>,编译器会根据这些初始化值推导出模板参数,并将其替换为相应的类型。

需要注意的是,类模板参数推导只适用于聚合体,对于非聚合体的类模板,仍然需要显式提供模板参数。

总而言之,在 C++17 中,我们可以使用聚合体进行模板参数推导,从而简化代码并提高可读性。通过利用聚合体的特征和初始化器列表,编译器能够自动推导出正确的模板参数,并为我们生成相应的具体化类模板。

57. Allow defaulting comparisons by value

C++20中引入了"Allow defaulting comparisons by value"这个新特性。

在C++11和C++14中,如果类没有定义比较运算符(如==,!=等),编译器不会自动为这个类生成默认的比较运算符。

而在C++20中,如果类满足以下条件,编译器会默认为这个类生成比较运算符:

  • 所有非静态数据成员可以比较
  • 没有用户定义的比较运算符
  • 没有用户定义的拷贝/移动构造函数或拷贝/移动赋值运算符

编译器会为这个类生成默认的==和!=运算符,它们会按成员依次进行值比较。

这个新特性的目的是:

  1. 如果类只包含可比较类型,默认就可以比较两个对象是否相等。

  2. 避免用户定义一些简单的比较运算符如==和!=。

  3. 与其他语言如Java、C#等一致,它们都会默认为值类型类生成比较运算符。

  4. 不影响已经定义比较运算符或有复杂成员的类。

例如:

struct Point {
  int x, y;
};

Point a{1, 2};
Point b{1, 2};

// 自动生成 == 和 != 运算符
if(a == b) {...}

所以C++20允许为满足条件的类默认生成比较运算符,简化了开发过程。

58. Remove std::weak_equality and std::strong_equality

C++20中删除了std::weak_equality和std::strong_equality这两个类型traits。

在C++11中,这两个类型traits被引入来支持三值比较(three-way comparison)。它们的定义如下:

  • std::weak_equality::value是一个布尔值,表示T是否支持弱等价关系(==和!=运算符)。

  • std::strong_equality::value是一个布尔值,表示T是否支持强等价关系(==运算符和严格弱序关系<)。

但是这两个traits实际上很少被使用,且其本身也存在一些问题:

  • 它们无法区分用户定义的运算符和默认生成的运算符。

  • 弱等价和强等价的概念在C++中没有很好地定义,容易产生歧义。

  • 大多数类型都不需要区分弱等价和强等价这两个概念。

基于这些原因,C++20决定删除这两个类型traits:

  • 不再支持通过traits判断类型是否支持三值比较。

  • 直接使用==和<运算符进行比较即可,不需要区分弱等价和强等价。

  • 保留std::partial_ordering等顺序关系类型来表示比较结果。

删除这两个traits可以简化C++类型系统,避免引入不必要的概念。开发者直接使用比较运算符就可以完成值的比较,不需要理会弱等价和强等价的区分。

59. Inconsistencies with non-type template parameters

C++模板中的非类型参数在C++11和之前标准版本中存在一些不一致性,C++20对此做出了一些修正:

  1. 非类型参数的类型必须是完整类型。

以前非类型参数允许使用未完整类型,这会导致难以理解的错误。C++20强制非类型参数必须是完整类型。

  1. 非类型参数的类型必须可以推导。

以前允许使用无法从模板实参推导出类型的非类型参数,这很难实现。C++20要求非类型参数类型必须可以从模板实参推导出来。

  1. 禁止非类型参数为引用类型。

以前允许非类型参数为引用类型,这会导致难以理解的行为。C++20明确禁止非类型参数为引用类型。

  1. 非类型参数的默认值类型必须与参数类型匹配。

以前默认值类型可以与参数类型不匹配,这很难实现并可能导致错误。C++20要求默认值类型必须与参数类型匹配。

  1. 非类型参数不能使用auto类型推导。

auto不能用于非类型参数类型,以避免歧义和难以推导默认值类型的情况。

  1. 明确非类型参数的作用域规则。

规定非类型参数的名称只在定义模板参数列表的范围内可见。

这些修正消除了非类型参数使用时一些不一致或难以理解的地方,使得非类型参数的语义定义得更清晰严格,有利于模板编程。

60. Pseudo-destructors end object lifetimes

DR98提出了一个关于虚析构函数和伪析构函数(pseudo-destructor)的语义问题,C++20对此做出了修正:

问题:

以前,调用一个对象的伪析构函数(通过->~T形式)不会结束该对象的生命周期。

例如:

struct S {
  virtual ~S();
};

S* p = new S;
p->~S(); 
// p仍然指向一个活动对象

这与我们通常的直觉不符,看起来p对象应该已经被销毁了。

修正:

C++20明确规定,调用一个对象的伪析构函数将会结束该对象的生命周期,相当于调用了delete运算符。

例如上面的例子,在p->~S()之后,p就不再指向一个活动对象,任何后续访问p都是未定义行为。

这与我们通常的直觉是一致的 - 调用析构函数看起来就像销毁了对象。

影响:

  • 修正后伪析构函数的语义与实际析构函数一致,消除歧义。

  • 可能需要修改一些依赖于旧语义的代码。

  • 有利于捕获更多使用伪析构函数不当的错误。

总体来说,这是一个清晰合理的修正,消除了虚析构函数语义上的一个歧义点。

61. Converting from T* to bool should be considered narrowing

DR11提出了一个关于指针到bool转换是否应视为窄化转换的问题。

原问题:

指针到bool转换(如if§) historically不视为窄化转换。

但指针值的范围通常比bool值大,所以这种转换可能导致信息丢失。

C++20的修正:

C++20明确规定,指针到bool的转换现在视为窄化转换。
影响:

这种转换现在需要显式强制转换,而不能隐式发生。

可能需要修改一些原来依赖隐式指针到bool转换的代码。

捕获一些潜在的bug,避免信息丢失。

更符合我们对“窄化转换”的理解 - 从范围更大的类型到范围更小的类型。

总体来说,这是一个合理的更正。将指针到bool视为窄化转换:

更清晰准确地定义了C++中的窄化转换概念

有助于避免潜在的bug由于信息丢失而产生

兼顾向下兼容考虑给予显式转换的“宽限期”

所以C++20在这方面做出的决定是可以接受的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值