C++20新语法

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,请先检查编译器是否支持。
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 表达式,从而获得更大的灵活性和通用性。
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 还引入了其他一些有关位域类型成员变量的改进和修复,例如对于位域类型成员变量的布局规则进行了更加具体的规定,以及修复了一些之前的实现问题。
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 的类型。
需要注意的是,对于非模板类的初始化列表构造函数,我们可以省略花括号。但是对于模板类来说,必须使用多重花括号来区分类模板参数和初始化列表。
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 的。否则会导致编译错误。
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 中的一个新特性,不是所有编译器都支持。
Lambdas in unevaluated contexts
在 C++ 中,有一些上下文是不会对表达式进行求值的(unevaluated contexts),例如类型或模板参数推导、sizeof 运算符等。在 C++20 中,我们可以在这些 unevaluated contexts 中使用 lambda 表达式,从而获得更大的灵活性和可读性。
假设我们要为一个函数指定返回类型,并且该返回类型取决于函数的参数类型。在 C++11 中,我们需要使用尾置返回类型语法来实现:
template
auto func(const T& t) -> decltype(t.size()) {
return t.size();
}
在 C++20 中,我们可以使用 unevaluated context 中的 lambda 表达式来简化这个过程:
template
auto func(const T& t) {
return [](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 表达式不会被执行,因此我们必须立即调用它并将参数传递给它,以确保其返回类型可以被推导出来。
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 等强度更高的比较类型来进一步约束运算符的行为,提高代码的可读性和可维护性。
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 来访问其中的一个变量。这样,我们就可以更清晰地表达我们的意图,并避免不必要的捕获。
需要注意的是,该特性只适用于单个变量的情况,如果我们需要捕获多个变量,则仍然需要使用捕获列表来指定它们。此外,该特性目前还没有得到所有编译器的完全支持,因此在使用时需要注意代码的兼容性。
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
#include

int main() {
std::vector 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循环更加灵活和强大。
Default constructible and assignable stateless lambdas
在C++中,无状态(stateless)的Lambda函数是指没有捕获任何外部变量的Lambda函数。默认可构造和可赋值的无状态Lambda函数是指这种Lambda函数对象可以通过默认构造函数创建,并且可以使用赋值运算符进行赋值操作。
从C++14开始,无状态Lambda函数可以被默认构造和赋值。这意味着您可以声明一个无参数的无状态Lambda函数,并使用默认构造函数进行初始化,或者将其赋值给其他相同类型的无状态Lambda函数对象。
以下是一个示例:
#include

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函数才可以默认构造和赋值。
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 成员变量也进行正确的拷贝操作,以保持对象状态的一致性。
Access checking on specializations
特化的访问检查是指对C++模板特化的访问权限进行检查的机制。在C++中,模板特化是一种为特定类型或条件提供专门定义的方式。然而,这些特化可能受到与原始模板不同的访问级别约束。
当通过模板特化提供私有(private)或受保护(protected)成员时,编译器将执行特化的访问检查。这意味着对于这些特化,只有在可以访问相应类或模板的作用域内,才能实例化并使用该特化。
下面是一个示例来说明特化的访问检查:
template
struct MyTemplate {
void DoSomething() { // 普通成员函数
std::cout << “Do something” << std::endl;
}
};

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

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

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

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

return 0;

}
在上面的示例中,我们有一个模板MyTemplate,它拥有一个普通的成员函数DoSomething()。然后,我们特化了该模板,以针对int类型提供了一个私有的成员函数 DoSomethingSpecialized() 并在公有成员函数DoSomething()中使用它。
在main()函数中,我们实例化了两个对象obj1和obj2,分别使用了普通版本和特化版本的DoSomething()函数。由于特化版本的私有成员函数的访问性,尝试访问obj2.DoSomething()会导致编译错误。
因此,特化的访问检查确保只能在具有适当访问权限的上下文中使用特化版本,从而提供了更严格的访问控制。
ADL and function templates that are not visible
ADL(Argument-Dependent Lookup)是C++中的一个机制,用于查找与函数参数相关的名称。当在函数调用中使用未声明的函数名时,编译器会在调用点所属的命名空间和参数类型的关联命名空间中进行搜寻相关的函数。
然而,在涉及到函数模板的情况下,如果函数模板本身不可见(即不在当前作用域内),则ADL无法生效。换句话说,对于不可见的函数模板,即使其模板参数类型匹配,编译器也不会扩展ADL来查找其关联的函数。取而代之的是,它将继续查找已经在作用域内可见的函数或函数模板。
这里有一个示例来说明这个问题:
namespace A {
struct Foo {};

// 不可见的函数模板
template 
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能够找到相关的函数模板定义。
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函数以满足编译时求值的要求。
Attributes [[likely]] and [[unlikely]]
[[likely]]和[[unlikely]]是C++17引入的属性(attributes),用于提供对条件分支的优化提示。
这些属性用于向编译器提供关于分支的概率信息,以帮助它在生成目标代码时进行优化。使用[[likely]]属性可以提示编译器某个条件的分支很可能会经常执行,而[[unlikely]]则提示编译器某个条件的分支很可能很少执行。
这些属性只是一种建议,并不保证编译器会按照提示进行优化。编译器可以使用这些提示来调整生成的代码,例如对于概率高的分支路径,它可以尝试将其放在跳转指令的目标位置附近,从而减少分支预测错误。
下面是一个示例,演示了如何使用[[likely]]和[[unlikely]]属性:
#include

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]]属性提示编译器该分支概率低。
请注意,这些属性的效果依赖于特定的编译器和目标体系结构,不同的编译器可能对这些属性有不同的实现方式和行为。因此,在使用时请仔细阅读编译器的文档,并进行性能测试和分析,以确定是否产生了预期的优化效果。
Make typename more optional
从C++17开始,模板代码中的typename关键字更加灵活,可以在某些情况下省略使用。在此之前,它在声明依赖类型时是必需的,但现在在特定上下文中可以省略typename关键字。
以下是可以省略typename关键字的主要情况:

  1. 在模板中引用非依赖基类名称或枚举器时:
    template
    struct MyStruct : T {
    void func() {
    baseFunction(); // 在访问继承成员时不需要"typename"
    }
    };
  2. 在返回类型后置语法中:
    template
    auto add(T t, U u) -> decltype(t + u) {
    return t + u;
    }
  3. 使用作用域解析运算符(::)访问嵌套类型或成员时:
    template
    void MyFunction() {
    typename T::NestedType nestedObj; // 此处"typename"是可选的
    nestedObj.memberFunction();
    }
  4. 在范围-based for 循环中:
    template
    void PrintElements(const Container& container) {
    for (auto it = container.begin(); it != container.end(); ++it) {
    std::cout << *it << ’ ';
    }
    }
    尽管在某些情况下可以省略typename关键字提供了一些方便,但请注意,在某些情况下仍然需要使用它。例如,在引用依赖类型名称或声明模板类型参数时,仍然需要使用typename关键字。
    请记住,即使在某些情况下可以省略typename,明确使用它也有助于提高代码的可读性,并使意图更加清晰。
    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

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

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]]属性,并通过测试和性能分析,确保达到预期的优化效果。
Conditionally Trivial Special Member Functions
在C++20之后,可以通过条件编译的方式标记特殊成员函数(Special Member Functions)为条件可平凡(conditionally trivial),即根据某些条件确定特殊成员函数是否为平凡的。
在条件可平凡特殊成员函数被声明为平凡时,编译器可以执行更多的优化,例如进行结构布局的优化和零初始化的优化。这对于具有特定需求的性能敏感代码可能是有益的。
下面是一个示例,展示了如何使用宏定义和std::is_trivially_constructible类型特性来条件地标记特殊成员函数为条件可平凡:
#include
#include

#define CONDITIONAL_TRIVIAL(clazz)
(std::is_trivially_constructible::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是平凡可构造的,将使用大括号内定义的默认构造函数。否则,将编译器生成的默认构造函数。
注意,在使用条件可平凡特殊成员函数时,需要确保对应的条件和标记是正确的,并进行充分的测试和验证以确保达到预期的行为。
请注意,以上示例中的宏定义只适用于默认构造函数。如果需要使用其他特殊成员函数(如复制构造函数、移动构造函数、析构函数等),类似的宏定义和类型特性检查可以进行相应的扩展。
最佳实践是在必要的情况下谨慎使用条件可平凡特殊成员函数,并针对具体需求进行性能测试和分析,以确定是否产生了预期的优化效果。
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循环。
    Allow structured bindings to accessible members
    C++20允许对可访问成员使用结构化绑定。
    在C++20中,结构化绑定语法得到了扩展,现在可以将其应用于类的可访问成员。结构化绑定是一种通过单个声明将多个变量绑定到一个复合类型(如结构体或类)的成员上的便捷方式。
    在之前的C++版本中,结构化绑定只能用于解构元组、数组等已存在的复合类型。但在C++20中,我们可以使用结构化绑定来直接访问类的成员,而无需通过getter函数或公共数据成员来访问。
    这种新的语法使得代码更加简洁和可读。通过结构化绑定,我们可以一次性地将类的多个成员绑定到不同的变量中,而不需要分别访问每个成员。
    下面是一个使用结构化绑定访问可访问成员的示例:
    #include

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允许对可访问成员使用结构化绑定,可以提高代码的可读性和简洁性,并且减少了访问类成员的冗余代码。
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

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中的析构删除机制提供了更灵活和可控的内存管理方式,使得我们可以更精确地控制对象的分配和释放过程,并且能够方便地使用类的成员变量和方法来完成自定义的操作。
Class types in Non-type template parameters
C++20引入了一项重要的功能,允许在非类型模板参数中使用类类型(Class types)。在此之前,非类型模板参数只能是整数、枚举类型或指针类型。现在,我们可以使用类类型作为非类型模板参数,这使得模板更加灵活和通用。
使用类类型作为非类型模板参数可以带来许多有用的特性。首先,我们可以将对象作为模板参数进行传递,而不仅仅限于指针或引用。这样可以让我们在编译时期就进行严格的类型检查,而不必依赖于运行时。
其次,类类型作为非类型模板参数还使得模板可以根据对象的不同进行特化。通过提供不同的实例化模板,我们可以根据不同的类类型参数生成不同的代码,从而提高代码的复用性和效率。
示例代码如下:
template
class MyClass {
public:
void printValue() {
std::cout << "Value: " << value << std::endl;
}
};

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

MyClass obj2;
obj2.printValue(); // 输出:Value: Hello

return 0;

}
在上面的示例中,MyClass 是一个接受类型模板参数 T 和非类型模板参数 value 的模板类。我们通过实例化 MyClass 并传递不同的类型和值来创建对象 obj1 和 obj2,然后调用 printValue 函数打印出对应的值。
需要注意的是,在使用类类型作为非类型模板参数时,必须保证该类类型是完整的、可求值的,并且可以在编译期间进行常量表达式求值。另外,类类型的默认构造函数必须是 constexpr 的,以便在编译时进行初始化。
总结而言,C++20中引入的非类型模板参数支持类类型的特性使得模板更加灵活和强大,可以用于更多的应用场景,从而提高代码的复用性和效率。
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包含在其中。
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_加上相应特性的名称,所有字母大写,并用下划线分隔单词。它们是由编译器定义的,因此在不同的编译器中可能会有所差异。
这些特性测试宏可以帮助开发者在编译时根据不同的编译环境选择不同的代码路径,以确保代码在不同的编译器和版本中都能正常运行。
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在编译时产生错误信息。
请注意,禁止聚合体的行为在不同的编译器和标准库实现中可能会有所不同。因此,最好在特定的平台上进行测试以确保预期的行为。
constexpr virtual function
C++20引入了对constexpr虚函数的支持。在C++中,constexpr关键字用于指示编译器在编译时计算表达式的值。虚函数是一种动态绑定的机制,允许子类覆盖父类的实现。通过将constexpr和虚函数结合起来,我们可以在编译时确定虚函数的调用结果。
以下是一个示例,展示如何在C++20中使用constexpr虚函数:
#include

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的。
Consistency improvements for comparisons
C++20为比较操作引入了一些一致性改进。这些改进的目的是提高比较操作的一致性和可读性。下面是一些主要的改进:

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

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

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()函数,我们可以在编译时获取结果并输出。
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引入的一种特殊函数类型,用于在编译期间进行立即求值。它们提供了更多的编译时计算和验证能力,有助于代码的优化和错误检查。
Nested inline namespaces
在C++20中,可以使用嵌套的内联命名空间(nested inline namespaces)来对命名空间进行更细粒度的组织和版本控制。
内联命名空间是指在一个命名空间中定义另一个命名空间,并且在使用时无需显式地指定外层命名空间的名称。而嵌套的内联命名空间则是在内联命名空间中再次定义内联命名空间。
下面是一个示例代码:
#include

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 版本。
通过使用嵌套的内联命名空间,我们可以更灵活地对代码进行版本管理和组织,而无需修改现有的代码结构。这对于库的演进和向后兼容性非常有用。
Yet another approach for constrained declarations
在C++20中,引入了一种新的语法来声明具有约束条件的变量和函数,这种语法被称为"constrained declarations"。它允许程序员在声明中指定对类型参数的约束条件,以限制类型参数的可能取值范围。
这种方法的主要思想是使用requires子句来定义约束条件。在类型参数后面使用requires关键字,然后在大括号内编写对类型参数的约束条件。例如:
template
requires std::is_integral_v
void foo(T value) {
// 函数体
}
这个例子中,我们使用requires子句来约束模板函数foo的类型参数T必须是整数类型。这样,只有传递整数类型的参数才能调用该函数。
此外,还可以在具体的声明中使用requires子句来限制类型参数的取值范围。例如:
template
concept Integral = std::is_integral_v;

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

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允许在常量表达式中更改联合体的活动成员,但仍需谨慎考虑使用场景和遵守语言规范。确保对联合体的操作在编译期间是可确定的,并且联合体满足字面类型的要求。

你可能感兴趣的:(c++20,数据结构)