跨越十年的C++演进:C++20新特性全解析

发布于:2025-07-04 ⋅ 阅读:(21) ⋅ 点赞:(0)

跨越十年的C++演进系列,分为5篇,本文为第四篇,后续会持续更新C++23~

前3篇如下:

跨越十年的C++演进:C++11新特性全解析

跨越十年的C++演进:C++14新特性全解析

跨越十年的C++演进:C++17新特性全解析

C++20标准是C++语言的第四个正式标准,于2020年12月正式发布。

首先先上C++20特性思维导图:

接下来将从关键字、语法、宏、属性、弃用这5个类目来讲解~

1、关键字

1.1、concept

编译器版本:GCC 10

concept 是 C++20 引入的重要特性之一,尤其适用于模板库的设计与开发。其功能类似于 C# 中的泛型约束,但相比而言更为灵活和强大。

concept 允许我们为模板参数定义具有特定条件的类型约束。

示例:数值类型的约束

#include <type_traits>
// 定义一个名为 number 的 concept,用于约束模板参数 T 必须是算术类型
template<typename T>
concept number = std::is_arithmetic_v<T>;
// 使用该 concept 来限制函数模板的参数类型
template<number T>
void func(T t)
{ }
// 调用示例
func<int>(10);        // 合法,int 属于算术类型
func<double>(20.0);   // 合法,double 属于算术类型
struct A { };
func<A>(A());         // 非法,A 不是算术类型,编译失败

1.2、requires

编译器版本:GCC 10

仅使用 concept 还不足以完全发挥其潜力,真正使其强大的是 requires 表达式。通过结合 concept 和 requires,可以对模板参数进行更细粒度的限制,包括检查成员变量、成员函数及其返回值等。

示例:约束类型必须具备某些成员函数或行为

#include <type_traits>
template<typename T>
concept can_run = requires(T t)
{
    std::is_class_v<T>;                     // 类型 T 必须是一个类或结构体
    t();                                    // 必须支持无参调用(括号运算符重载)
    t.run();                                // 必须包含 run 成员函数
    std::is_same_v<decltype(t.run()), int>; // run 函数返回类型必须为 int
};
// 使用该 concept 的函数模板
template<can_run T>
int func(T t)
{
    t();
    return t.run(); // 可以直接返回 run() 的结果,因为其返回类型已被限定为 int
}
func<int>(10); // 错误,int 不是 class 或 struct
struct A {
    void run() { }
};
func<A>(A()); // 错误,缺少括号运算符重载
struct B {
    void operator()() { }
};
func<B>(B()); // 错误,缺少 run 函数
struct C {
    void operator()() { }
    void run() { }
};
func<C>(C()); // 错误,run 返回类型不是 int
struct D {
    int operator()() { }
    int run() { return 0; }
};
func<D>(D()); // 正确,满足所有条件

1.3、typename

编译器版本:GCC 9

typename 在模板中主要有两个用途:一是作为模板参数声明;二是明确告诉编译器某个嵌套名称是一个类型名。在早期版本中,为了避免歧义,需要频繁使用 typename 来辅助编译器解析。而新版本增强了类型推导能力,使得部分场景下可以省略 typename。

例如,在只能推导出类型的上下文中,可不加 typename。

示例:

// 函数返回类型位于全局作用域,只可能是类型,因此无需 typename
template<class T> T::R f(); // 合法
// 作为函数参数时则需要显式指定为类型
template<class T> void f(T::R); // 非法,无法推断为类型
template<typename T>
struct PtrTraits {
    using Ptr = T*;
};
template<class T>
struct S {
    using Ptr = PtrTraits<T>::Ptr; // 合法,在 defining-type-id 上下文中
    T::R f(T::P p) {
        return static_cast<T::R>(p); // 合法,在函数体内也可识别为类型
    }
    auto g() -> S<T*>::Ptr; // 合法,尾随返回类型
};
template<typename T>
void f() {
    void (*pf)(T::X);     // 合法,pf 是指向函数的指针
    void g(T::X);         // 非法,T::X 无法被解释为类型
}

1.4、explicit

编译器版本:GCC 9

新增于 C++11 版本,具体可参考 C++11 新特性相关内容。

C++20 中扩展了 explicit,允许传入一个布尔值来控制是否启用显式构造行为。

示例:

struct A {
    explicit(false) A(int) { } // 允许隐式转换
};
struct B {
    explicit(true) B(int) { } // 禁止隐式转换
};
A a = 10; // 合法,允许隐式构造
B b = 10; // 非法,禁止隐式构造

1.5、constexpr

编译器版本:GCC 9

该特性最初在 C++11 中引入,如需详细了解可参考 C++11 的新特性。

① 在 C++20 中,constexpr 的使用范围得到了进一步扩展,新增了对虚函数的支持。其用法与普通函数一致,无需额外说明。

② constexpr 函数中不再允许使用 try-catch 语句块。此限制也适用于构造函数和析构函数中的异常处理逻辑。

1.6、char8_t

编译器版本:GCC 9

char8_t 是为 UTF-8 编码专门设计的新类型。今后,UTF-8 字符字面量将由 char8_t 类型接收,而不再是 char 类型。

目前 GCC 编译器对该特性的支持尚未完全实现,相关内容仍在持续完善中。


1.7、consteval

编译器版本:GCC 11

consteval 关键字用于定义“立即函数”,这类函数必须在编译期执行完毕,并返回一个编译期常量结果。函数参数也必须是能够在编译期确定的值,且函数内部的所有运算都必须能在编译期完成。

相较于 constexpr,consteval 对函数的限制更加严格。constexpr 函数会根据传入参数是否为常量表达式自动决定是在编译期还是运行期执行;而 consteval 函数则强制要求所有调用都必须发生在编译期。

示例代码:

#include <iostream>
constexpr int f(int a)
{
    return a * a;
}
// 参数a必须是编译期常量
consteval int func(int a)
{
    return f(a); // 合法,因为f()可以在编译期计算
}
int main()
{
    int a;
    std::cin >> a;
    int r1 = f(a);             // 合法,a是运行期变量,f将在运行期执行
    int r2 = func(a);          // 错误,a不是编译期常量
    int r3 = func(1000);       // 合法,1000是常量
    int r4 = func(f(10));      // 合法,f(10)在编译期完成,符合consteval要求
    return 0;
}

1.8、co_await、co_yield、co_return

编译器版本:GCC 10
编译选项:-fcoroutines

协程三大关键字:co_await、co_yield 和 co_return。

1.8.1、语法示例

(先了解基本语法,后文将详细解释)

using namespace std::chrono;
struct TimeAwaiter 
{
    std::chrono::system_clock::duration duration;
    bool await_ready() const 
    { return duration.count() <= 0; }
    void await_resume() {}
    void await_suspend(std::coroutine_handle<> h) {}
};
template<typename _Res>
struct FuncAwaiter 
{
    _Res value;
    bool await_ready() const 
    { return false; }
    _Res await_resume() 
    { return value; }
    void await_suspend(std::coroutine_handle<> h) 
    { std::cout << __func__ << std::endl; }
};
TimeAwaiter operator co_await(std::chrono::system_clock::duration d) 
{
    return TimeAwaiter{d};
}
static FuncAwaiter<std::string> test_await_print_func()
{
    std::this_thread::sleep_for(1000ms);
    std::cout << __func__ << std::endl;
    return FuncAwaiter<std::string>{std::string("emmmmmmm ") + __func__};
}
static generator_with_arg f1() 
{ 
    std::cout << "11111" << std::endl;
    co_yield 1; 
    std::cout << "22222" << std::endl;
    co_yield 2; 
    std::cout << "33333" << std::endl;
    co_return 3;
}
static generator_without_arg f2() 
{ 
    std::cout << "44444" << std::endl;
    std::cout << "55555" << std::endl;
    std::cout << "66666" << std::endl;
    co_return;
}
static generator_without_arg test_co_await() 
{
    std::cout << "just about go to sleep...\n";
    co_await 5000ms;
    std::cout << "resumed 1111\n";
    std::string ret = co_await test_await_print_func();
}

总结:

  • co_return [result]
    表示协程最终的返回结果。若未指定值,则默认为 void。
  • co_yield value
    表示协程挂起时返回的值。不可省略,且必须与 co_return 返回类型一致。当 co_return 返回类型为 void 时,不能使用 co_yield。
  • co_await value
    • 可以被重载,重载函数应返回一个 awaiter。
    • 若 value 是某个函数调用,则该函数必须返回一个 awaiter。

1.8.2、awaiter 说明

一个合法的 awaiter 类型必须实现以下三个接口函数:

  • await_ready()
    首先被调用,返回一个布尔值。若为 true,表示操作已完成,继续执行后续代码;若为 false,则协程将被挂起,并进入 await_suspend()。
  • await_suspend(h)
    接收当前协程的句柄 h。返回类型可以是 void 或 bool。若返回 false,协程不会挂起;若返回 void,等效于返回 true,即协程挂起。
  •  std::coroutine_handle<> 是标准库提供的类型,用于引用协程对象,控制其生命周期和唤醒行为。
  • await_resume()
    当协程恢复执行时调用。其返回值就是 co_await 表达式的返回值。

1.8.3、协程函数

协程函数的返回类型必须包含一个名为 promise_type 的嵌套类型。这个 promise_type 负责管理协程的状态和返回值。

编译器会自动调用 promise_type::get_return_object() 来获取协程函数的返回值(通常是 generator 类型),用户无需手动编写 return 语句。

通常情况下,generator 类型需要保存协程的句柄,以便外部程序控制协程的执行流程。

1.9、constinit

编译器版本:GCC 10

constinit 用于确保变量在编译期完成初始化,禁止动态初始化。

适用条件:
变量必须具有静态存储周期或线程局部存储周期(thread_local)。thread_local 变量可以选择不初始化。

示例代码:

const char * get_str1()
{
    return "111111";
}
constexpr const char * get_str2()
{
    return "222222";
}
const char *hahah = " hhahahaa ";
constinit const char *str1 = get_str2(); // 合法,使用 constexpr 函数初始化
constinit const char *str2 = get_str1(); // 非法,使用非 constexpr 函数初始化
constinit const char *str3 = hahah;      // 非法,使用非常量表达式初始化
int main()
{
    static constinit const char *str4 = get_str2(); // 合法,静态变量
    constinit const char *str5 = get_str2();        // 非法,非静态/非 thread_local
    constinit thread_local const char *str6;        // 合法,thread_local 可不初始化
    return 0;
}

2、语法

2.1、位域变量的默认成员初始化

编译器版本:GCC 8

C++20 允许在定义位域变量时为其指定默认初始值。这一特性提升了代码的可读性和安全性。

声明语法格式如下:

类型 变量名 : 位数 = 初始值;
类型 变量名 : 常量表达式 {初始值};

示例:

int a;
const int b = 1;
struct S
{
    int x1 : 8 = 42;            // 合法,x1 是一个 8 位整型,并被初始化为 42
    int x2 : 6 {42};            // 合法,同上,使用花括号初始化
    int x3 : true ? 10 : a = 20; // 合法,三目运算结果是 10,未进行赋值操作(优先级问题)
    int x4 : true ? 10 : b = 20; // 非法,b 是 const,不能对其赋值
    int x5 : (true ? 10 : b) = 20; // 合法,强制优先级后,位宽为 10,并初始化为 20
    int x6 : false ? 10 : a = 20; // 非法,a = 20 不是常量表达式
};

2.2、修改 const 限定的成员指针

编译器版本:GCC 8

在 C++20 中,允许对 .* 表达式中的第二个操作数进行更灵活的处理。特别是当该操作数是一个指向带有 & 限定符的成员函数指针时,只有在其具有 const 限定的情况下才是合法的。

示例:

struct S {
    void foo() const& { }
};
void f()
{
    S{}.foo();           // 合法,调用 const& 成员函数
    (S{}.*&S::foo)();    // C++20 起支持这种语法
}

2.3、允许 lambda 表达式按值捕获 this

编译器版本:GCC 8

lambda 表达式现在可以显式地按值捕获 this 指针,这使得 lambda 在捕获对象状态时更加清晰明确。

示例:

struct S
{
    int value;
    void print()
    {
        auto f = [=, this]() {
            this->value++;
        };
    }
};

上述代码中,[=, this] 表示以值方式捕获所有外部变量,并且显式捕获 this 指针。


2.4、指定初始化

编译器版本:GCC 8

C++20 引入了类似 C99 的“指定初始化”语法,允许在构造聚合类型时通过字段名称来初始化特定成员。但必须按照成员在类或结构体中定义的顺序进行初始化。

示例:

struct A { int x, y; };
struct B { int y, x; };
void f(A a, int);  // #1
void f(B b, ...);  // #2
void g(A a);       // #3
void g(B b);       // #4
void h()
{
    f({.x = 1, .y = 2}, 0);         // 合法,调用 #1
    f({.y = 1, .x = 2}, 0);         // 非法,成员顺序与定义不一致,无法匹配 #1
    f({.y = 1, .x = 2}, 1, 2, 3);   // 合法,调用 #2
    g({.x = 1, .y = 2});            // 非法,无法确定调用 #3 还是 #4
}

2.5、lambda 表达式支持模板

编译器版本:GCC 8

C++20 支持在 lambda 表达式中使用模板参数,从而实现泛型 lambda。这一特性极大增强了 lambda 的灵活性和通用性。

示例 1:

int a;
auto f = [&a]<typename T>(const T &m) {
    a += m;
};
f(10); // 正确,T 推导为 int

示例 2:

template<typename T>
int func(int t) 
{
    return t * t;
}
int f()
{
    return func<decltype([] {})>(20); // 使用 lambda 类型作为模板参数
}

示例 3:

using A = decltype([] {});
void func(A *) { }
func(nullptr);
template<typename T>
using B = decltype([] {});
void f1(B<int> *) { }
template<typename T>
void f2(B<T> *) { }
f1(nullptr);          // 合法
f2<int>(nullptr);     // 合法

2.6、从构造函数推导出模板参数类型

编译器版本:GCC 8

C++20 允许在变量声明时省略模板参数,编译器将根据构造函数参数自动推导出实际类型。

示例:

vector v{vector{1, 2}}; // 合法,v 被推导为 vector<vector<int>>
tuple t{tuple{1, 2}};   // 合法,t 被推导为 tuple<int, int>

2.7、简化 lambda 的隐式捕获

编译器版本:GCC 8

本节内容较为复杂,涉及 lambda 表达式捕获机制的改进。由于篇幅限制,此处不再详细展开。

如需深入了解,请参考提案文档:

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html

2.8、ADL与不可见的模板函数

编译器版本:GCC 9

ADL(Argument-Dependent Lookup)是 C++ 中的一种机制,用于自动解析调用函数的位置,简化代码编写。C++20 扩展了这一机制,使得它也可以应用于模板函数的推导。

示例:

int h;
void g();
namespace N {
    struct A {};
    template<typename T> int f(T);
    template<typename T> int g(T);
    template<typename T> int h(T); // 注意这里的 h 是一个模板函数
}
int x = f<N::A>(N::A()); // 正确,调用了 N::f
int y = g<N::A>(N::A()); // 正确,调用了 N::g
int z = h<N::A>(N::A()); // 错误,因为全局命名空间中的 h 被认为是一个变量而非模板函数

2.9、operator<=>

由于篇幅限制,关于 operator<=> 的详细讨论请参考官方文档:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r3.pdf

2.10、基于范围的 for 循环初始化

编译器版本:GCC 9

C++20 引入了一种新的基于范围的 for 循环语法,允许在循环开始前执行初始化语句。

新语法格式如下:

for([init-statement;] for-range-declaration : for-range-initializer) ...

示例:

int a[] = {1, 2, 3, 4};
for(int b = 0; int i : a) {
    // 使用 i 和 b 进行操作
}

2.11、默认可构造可分配的无状态 lambdas

编译器版本:GCC 9

C++20 允许获取 lambda 表达式的类型,并创建该类型的对象,即使这个 lambda 没有捕获任何外部变量。

示例:

#include <iostream>
#include <map>
auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> map;
static void f() {}
int main() {
    decltype(f) ff;
    ff(); // 调用静态函数
    decltype(greater) d;
    d(10, 20); // 调用比较操作
    return 0;
}

2.12、专门的访问检查

此特性已在 GCC 和 MSVC 编译器中实现,但在其他编译器中可能未完全支持。它允许在模板类内部忽略访问权限来访问另一个类的嵌套类。

示例:

class A {
    struct impl1 { int value; };
    template<typename T> class impl2 { T value; };
    class impl3 { int value; };
};
struct B {
    A::impl1 t; // 错误:'struct A::impl1' 在此上下文中为私有
};
template<typename T>
struct trait {
    A::impl1 t;     // 正确
    A::impl2<T> t2; // 正确
    void func() {
        A::impl1 tmp;  // 正确
        tmp.value = 10;// 正确
        t2.value = 20; // 正确
        A::impl3 t3;   // 正确
        t3.value = 30; // 正确
    }
};
int main() {
    trait<int> a;
    a.t.value = 10; // 正确
    a.t2.value = 20; // 错误:'int A::impl2<int>::value' 在此上下文中为私有
    return 0;
}

2.13、constexpr 函数的实例化

编译器版本:GCC 9

当仅需获取 constexpr 函数的返回值类型时,无需实例化整个函数,只需推导其返回类型即可。

示例:

template<typename T> 
constexpr int f() { return T::value; }
// 此处仅推导 f<T>() 的返回值类型
template<bool B, typename T> 
void g(decltype(B ? f<T>() : 0)) { }
template<bool B, typename T> void g(...) { }
// 因为需要实际获取 int 类型的数据,所以必须实例化 f<T>()
template<bool B, typename T> void h(decltype(int{B ? f<T>() : 0})) { }
template<bool B, typename T> void h(...) { }
void x() {
    g<false, int>(0); // OK,因为不需要实例化 f<int>()
    h<false, int>(0); // 错误,即使 B 为 false,也需要实例化 f<int>()
}

2.14、允许 lambda 在初始化捕获时进行包扩展

编译器版本:GCC 9

C++20 扩展了包扩展的应用场景,现在可以在 lambda 初始化捕获时使用包扩展。

示例:

#include <functional>
template<class F, class... Args>
auto invoke1(F f, Args... args) {
    return [f, args...]() -> decltype(auto) {
        return std::invoke(f, args...);
    };
}
template<class F, class... Args>
auto invoke2(F f, Args... args) {
    return [f=std::move(f), ...args=std::move(args)]() -> decltype(auto) {
        return std::invoke(f, args...);
    };
}
template<class F, class... Args>
auto invoke3(F f, Args... args) {
    return [f=std::move(f), tup=std::make_tuple(std::move(args)...)]() -> decltype(auto) {
        return std::apply(f, tup);
    };
}

2.15、放宽结构化绑定,新增自定义查找规则

编译器版本:GCC 8

C++20 放宽了结构化绑定的限制,并允许自定义查找规则以适应更复杂的绑定需求。

自定义条件包括:

  1. 实现 get<int>(Type) 函数或 Type::get<int>() 成员函数。
  2. 特化 tuple_size 和 tuple_element 结构体。
  3. 确保 get<int> 返回路径数量与 tuple_size 指定的数值一致。
  4. get<int N>
  5.  函数返回类型应与 tuple_element 对应索引指定的类型相同。

示例 1:

#include <string>
#include <tuple>
struct A {
    int a;
    int b;
};
struct X : private A {
    std::string value1;
    std::string value2;
};
template<int N> 
auto& get(X &x) {
    if constexpr (N == 0)
        return x.value2;
}
namespace std {
    template<> 
    class tuple_size<X> : public std::integral_constant<int, 1> {};
    template<> 
    class tuple_element<0, X> {
    public: 
        using type = std::string;
    };
}
int main() {
    X x;
    auto& [y] = x; // y 的类型为 string
    auto& [y1, y2] = x; // 错误:提供了 2 个名称进行结构化绑定,而 'X' 解构为 1 个元素
    return 0;
}

示例 2:

#include <string>
#include <tuple>
struct A {
    int a;
    int b;
};
struct X : protected A {
    std::string value1;
    std::string value2;
    template<int N> 
    auto& get() {
        if constexpr (N == 0)
            return value1;
        else if constexpr (N == 1)
            return a;
    }
};
namespace std {
    template<> 
    class tuple_size<X> : public std::integral_constant<int, 2> {};
    template<> 
    class tuple_element<0, X> {
    public: 
        using type = std::string;
    };
    template<> 
    class tuple_element<1, X> {
    public: 
        using type = int;
    };
}
int main() {
    X x;
    auto& [y1, y2] = x; // y1 为 string 类型,y2 为 int 类型
    return 0;
}

2.16、放宽基于范围的 for 循环,新增自定义范围方法

编译器版本:GCC 8

在 C++20 中,允许不通过类内部的 begin() 和 end() 成员函数来实现基于范围的 for 循环。现在可以将这两个函数实现在类外部,依然可以被正确识别。

示例:

#include <iostream>
struct X {
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    int e = 5;
};
int* begin(X& x) {
    return reinterpret_cast<int*>(&x);
}
int* end(X& x) {
    return reinterpret_cast<int*>(&x) + sizeof(x) / sizeof(int);
}
int main() {
    X x;
    for (int i : x) {
        std::cout << i << std::endl;
    }
    std::cout << "finish" << std::endl;
    return 0;
}

2.17/类类型的非类型模板参数

编译器版本:GCC 9

C++20 允许使用类类型作为非类型模板参数,前提是该类的所有操作可以在编译期完成,并且满足特定条件(如支持常量比较等)。

基本用法示例:

#include <iostream>
struct A {
    int value;
    // operator== 必须是 constexpr
    constexpr bool operator==(const A& v) const {
        return value == v.value;
    }
};
template<A a, A b>
struct Equal {
    static constexpr bool value = a == b; // 编译期比较
};
int main() {
    static constexpr A a{10}, b{20}, c{10};
    std::cout << std::boolalpha;
    std::cout << Equal<a, b>::value << std::endl; // 输出 false
    std::cout << Equal<a, a>::value << std::endl; // 输出 true
    return 0;
}

① operator== 的缺口与地址一致性

当使用类类型作为非类型模板参数时,如果两个对象逻辑上相等(即 <=> 比较结果为 0),那么它们实例化的模板也应共享相同的地址。

#include <iostream>
template<auto v>
int Value;
struct A {
    int value;
};
int main() {
    static constexpr A a{10}, b{20}, c{10};
    std::cout << std::boolalpha;
    std::cout << (&Value<a> == &Value<b>) << std::endl; // false
    std::cout << (&Value<a> == &Value<c>) << std::endl; // true
    return 0;
}

② 成员函数调用要求 constexpr

由于模板参数必须在编译期求值,因此类中的成员函数用于模板参数计算时,必须标记为 constexpr。

③ 类模板参数的相互推导

C++20 支持从字符串字面量等自动推导模板参数。

#include <string>
template<typename _Tp, std::size_t N>
struct MyArray {
    constexpr MyArray(const _Tp (&foo)[N + 1]) {
        std::copy_n(foo, N + 1, m_data);
    }
    auto operator<=>(const MyArray&, const MyArray&) = default;
    _Tp m_data[N];
};
template<typename _Tp, std::size_t N>
MyArray(const _Tp (&str)[N]) -> MyArray<_Tp, N - 1>;
template<std::size_t N>
using CharArray = MyArray<char, N>;
// 旧写法需要显式指定大小
template<std::size_t N, CharArray<N> Str>
struct A {};
using hello_A = A<5, "hello">;
// 新写法可自动推导
template<CharArray Str>
struct B {};
using hello_B = B<"hello">;

④ 用户自定义字面量支持

结合上述特性,可以实现基于字符串字面量的模板参数化处理:

template<CharArray Str>
auto operator""_udl();
"hello"_udl; // 等价于 operator""_udl<"hello">()

类类型非类型模板参数的条件(满足任意一个即可):

  • 是字面量类型;
  • 是左值引用;
  • 包含占位符类型;
  • 是派生类类型的占位符;
  • 拥有强结构可比较性(即支持默认的 operator<=>,没有 mutable 或 volatile 成员);

强结构可比较性的定义:对于任意类型 T,若 const T 的 glvalue 对象 x,使得 x <=> x 返回 std::strong_ordering 或 std::strong_equality,并且不调用用户定义的三向比较运算符或结构化比较函数,则该类型具有强结构可比较性。

2.18、禁止使用用户声明的构造函数进行聚合初始化

编译器版本:GCC 9

在 C++20 中,禁止使用用户显式声明的构造函数(即使为 default 或 delete)来进行聚合初始化,从而修复了一些边缘情况下的语义不一致问题。

旧版存在的几个问题:

① delete 构造函数仍可聚合初始化

struct X {
    X() = delete;
};
int main() {
    X x1;       // 错误:X() 被删除
    X x2{};     // 旧版本可能通过编译(错误)
}

② 双重聚合初始化

struct X {
    int i{4};
    X() = default;
};
int main() {
    X x1(3);   // 错误:没有匹配的构造函数
    X x2{3};   // 旧版本可能通过编译(错误)
}

③ 类外 default 构造函数导致聚合失败

struct X {
    int i;
    X() = default;
};
struct Y {
    int i;
    Y();
};
Y::Y() = default;
int main() {
    X x{4};   // 旧版本可能通过编译(错误)
    Y y{4};   // 旧版本可能报错(不一致)
}

C++20 的修正方案:

如果类中显式声明了除拷贝/移动构造函数以外的其他构造函数,则该类不能使用聚合初始化。

修正后的行为如下:

struct X {
    X() = delete;
};
int main() {
    X x1;      // 错误:构造函数被删除
    X x2{};    // 错误:构造函数被删除
}
struct X {
    int i{4};
    X() = default;
};
int main() {
    X x1(3);   // 错误:无匹配构造函数
    X x2{3};   // 错误:不允许聚合初始化
}
#include <initializer_list>
struct X {
    int i;
    X() = default;
};
struct Y {
    int i;
    Y();
};
Y::Y() = default;
struct A {
    int i;
    A(int);
};
struct B {
    int i;
    B(int);
};
B::B(int) {}
struct C {
    int i;
    C() = default;
    C(std::initializer_list<int> list);
};
int main() {
    X x{4};    // 错误:无匹配构造函数
    Y y{4};    // 错误:无匹配构造函数
    A a{5};    // 正确
    B b{5};    // 正确
    C c{6};    // 正确:支持 initializer_list 构造函数
    return 0;
}

2.19、嵌套内联命名空间

编译器版本:GCC 9

C++20 引入了嵌套内联命名空间的新语法,使得在定义多个层级的命名空间时更加简洁,并且可以更灵活地控制符号的可见性。

旧写法(使用 inline关键字):

#include <iostream>
namespace A {
    inline namespace B {
        void func() {
            std::cout << "B::func()" << std::endl;
        }
    } // namespace B
} // namespace A
int main() {
    A::func(); // 输出 B::func()
    return 0;
}

新特性写法(支持 A::inline C写法):

#include <iostream>
namespace A {
    namespace B {
        void func() {
            std::cout << "B::func()" << std::endl;
        }
    } // namespace B
} // namespace A
namespace A::inline C {
    void func() {
        std::cout << "C::func()" << std::endl;
    }
}
int main() {
    A::func(); // 输出 C::func()
    return 0;
}
  • A::inline C 是 namespace A { inline namespace C { ... } } 的简写形式。
  • 内联命名空间中的函数/变量默认在父命名空间中可见,用于实现向后兼容或接口简化。

2.20、约束声明的另一种办法

编译器版本:GCC 10

C++20 支持将 auto 和 concept 结合使用,从而简化模板约束的写法,使代码更具表现力和可读性。

示例:

#include <iostream>
struct Compare {
    // 使用 auto 替代模板类型,自动推导参数
    bool operator()(const auto& t1, const auto& t2) const {
        return t1 < t2;
    }
};
template<typename T>
concept CanCompare = requires(T t) {
    t * t;  // 需要支持乘法运算符
    Compare().operator()(T(), T()); // 需要支持 < 运算符
};
// 使用 concept + auto 的函数返回值和参数
CanCompare auto pow2(CanCompare auto x) {
    CanCompare auto y = x * x;
    return y;
}
struct A {
    int value = 0;
    bool operator<(const A& a) const {
        return value < a.value;
    }
    A operator*(const A& a) const {
        return {.value = a.value * this->value};
    }
};
int main() {
    A a;
    a.value = 100;
    A aa = pow2(a); // 推导 x 为 A 类型,满足 CanCompare 约束
    std::cout << aa.value << std::endl;
    return 0;
}

2.21、允许在常量表达式中使用 dynamic_cast、多态 typeid

编译器版本:GCC 10

C++20 允许在 constexpr 上下文中使用 dynamic_cast 和 typeid,前提是它们的操作对象是已知的静态类型或具有多态性的对象。

示例:

#include <iostream>
#include <typeinfo>
struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {};
constexpr bool test_dynamic_cast() {
    Derived d;
    Base* b = &d;
    return dynamic_cast<Derived*>(b) != nullptr;
}
static_assert(test_dynamic_cast(), "dynamic_cast in constexpr failed");
constexpr bool test_typeid() {
    Derived d;
    Base* b = &d;
    return typeid(*b) == typeid(Derived);
}
static_assert(test_typeid(), "typeid in constexpr failed");

2.22、允许用圆括号的值进行聚合初始化

编译器版本:GCC 10

C++20 扩展了聚合初始化语法,允许使用圆括号 ( ) 来代替花括号 { },只要目标类型是聚合类型。

示例:

#include <iostream>
struct A {
    int v;
};
struct B {
    int a;
    double b;
    A&& c;
    long long&& d;
};
A get() {
    return A();
}
int main() {
    int i = 100;
    B b1{1, 20.0, A(), 200};         // 正常聚合初始化
    B b2(1, 20.0, A(), 300);         // C++20 新增支持
    B b3{1, 20.0, get(), 300};       // 正常
    B b4(2, 30.0, std::move(get()), std::move(i)); // 正常
    return 0;
}

2.23、new 表达式的数组元素个数推导

编译器版本:GCC 11

C++20 支持在使用 new 创建数组时省略大小,由编译器根据初始化列表自动推导。

示例:

#include <iostream>
#include <cstring>
int main() {
    double a[]{1, 2, 3};              // 普通数组推导
    double* p = new double[]{1, 2, 3}; // 自动推导大小为 3
    p = new double[0]{};              // 显式指定大小为 0
    p = new double[]{};               // 自动推导大小为 0
    char* d = new char[]{"Hello"};   // 推导为包含 '\0' 的字符串数组
    int size = std::strlen(d);
    std::cout << size << std::endl;   // 输出 5
    return 0;
}

2.24、Unicode 字符串字面量

编译器版本:GCC 10

C++20 支持 UTF-16 和 UTF-32 编码的字符串字面量,分别使用前缀 u 和 U。

示例:

#include <string>
int main() {
    std::u16string str1 = u"aaaaaa"; // UTF-16 字符串
    std::u32string str2 = U"bbbbbb"; // UTF-32 字符串
    return 0;
}

2.25、允许转换成未知边界的数组

编译器版本:GCC 10

C++20 允许将数组作为实参传递给接受未知边界数组的形参。这种特性对于泛型编程非常有用。

示例:

template<typename T>
static void func(T (&arr)[]) {
    // 接收任意大小的数组
}
template<typename T>
static void func(T (&&arr)[]) {
    // 接收临时数组
}
int main() {
    int a[3];
    int b[6];
    func<int>(a);                // OK
    func<int>(b);                // OK
    func<int>({1, 2, 3, 4});     // OK
    func<double>({1.0, 2, 3, 4, 8.0}); // OK
    return 0;
}

2.26、聚合初始化推导类模板参数

编译器版本:GCC 8

C++20 支持通过聚合初始化的方式推导类模板参数类型,提升了模板编程的灵活性。

示例:

template <typename T>
struct S {
    T x;
    T y;
};
template <typename T>
struct C {
    S<T> s;
    T t;
};
template <typename T>
struct D {
    S<int> s;
    T t;
};
C c1 = {1, 2};             // error: deduction failed
C c2 = {1, 2, 3};          // error: deduction failed
C c3 = {{1u, 2u}, 3};      // OK: 推导为 C<int>
D d1 = {1, 2};             // error: deduction failed
D d2 = {1, 2, 3};          // OK: 推导为 D<int>
template <typename T>
struct I {
    using type = T;
};
template <typename T>
struct E {
    typename I<T>::type i;
    T t;
};
E e1 = {1, 2};             // OK: 推导为 E<int>

2.27、隐式地将返回的本地变量转换为右值引用

编译器版本:GCC 11

C++20 引入了一项优化:在某些情况下,函数中返回局部变量时会自动使用移动语义(move)而不是复制(copy),即使没有显式使用 std::move。

触发条件:

以下两种情况会触发隐式 move:

  1. return 或 co_return 表达式中的 id-expression 是函数最内层块或 lambda 主体中声明的“隐式可移动实体”;
  2. throw 表达式中引用的是一个“隐式可移动实体”,并且该实体的作用域不超过其所在的 try 块或构造函数初始化列表。

✅ 隐式可移动实体定义:

是局部变量;

没有被 const 修饰;

不是数组;

不是通过花括号 {} 初始化的聚合类型;

没有绑定到引用;

没有被取地址(&)操作符使用过。

示例:

#include <iostream>
struct base {
    base() {}
    base(const base&) {
        std::cout << "base(const base &)" << std::endl;
    }
private:
    base(base&&) noexcept {
        std::cout << "base(base &&)" << std::endl;
    }
};
struct derived : base {};
base f() {
    base b;
    throw b; // 自动调用移动构造函数(如果可用)
    derived d;
    return d; // 自动调用移动构造函数(从 derived -> base)
}
int main() {
    try {
        f();
    } catch (base) {
        // 输出两次 "base(base &&)"
    }
    return 0;
}

2.28、允许 default修饰按值比较的运算符

编译器版本:GCC 10

C++20 支持对按值传递参数的比较运算符使用 = default,用于自动生成默认实现。

示例:

struct C {
    friend bool operator==(C, C) = default; // 合法!C++20 起支持
};

2.29、非类型模板参数等效的条件

编译器版本:GCC 10

当两个非类型模板参数的值被认为是“等效”的时候,它们可以被视为相同的模板实参。这在模板特化、别名推导等场景中非常重要。

等效条件(满足任意一条即可):

类型

判断条件

整型

值相同

浮点型

值相同

std::nullptr_t

都为 nullptr

枚举类型

枚举值相同

指针类型

指向同一对象或函数

成员指针类型

指向同一个类的同一成员,或者都为空

引用类型

引用同一个对象或函数

数组类型

所有元素都满足等效条件

共用体类型

没有活动成员,或具有相同的活动成员且其值等效

类类型

所有直接子对象和引用成员满足等效条件

2.30、Destroying Operator Delete

编译器版本:GCC 9

C++20 引入了新的 operator delete 形式:destroying operator delete,它允许在析构对象的同时控制内存释放行为。

语法格式:

void operator delete(T* ptr, std::destroying_delete_t);

特点:

  1. 优先级高于普通 operator delete
  2. 不会自动释放内存
  3. ,只负责销毁对象;
  4. 支持虚析构函数的行为一致性
  5. (即遵循多态规则);
  6. 仅适用于非数组类型的 delete 操作。

示例 1:基本用法

#include <iostream>
#include <new> // 包含 std::destroying_delete_t
struct A {
    void operator delete(void* ptr) {
        std::cout << "111" << std::endl;
    }
    void operator delete(A* ptr, std::destroying_delete_t) {
        std::cout << "222" << std::endl;
    }
};
struct B {
    int value = 10;
    void operator delete(B* ptr, std::destroying_delete_t) {
        std::cout << "333" << std::endl;
    }
};
struct C {
    void operator delete(void* ptr) {
        std::cout << "444" << std::endl;
    }
};
int main() {
    A* a = new A;
    delete a; // 输出 222
    B* b = new B;
    b->value = 100;
    delete b; // 输出 333,b->value 仍可访问(未释放内存)
    std::cout << b->value << std::endl; // 输出 100
    C* c = new C;
    delete c; // 输出 444
    return 0;
}

示例 2:继承与虚析构函数

#include <iostream>
#include <new>
struct A {
    virtual ~A() {}
    void operator delete(A* ptr, std::destroying_delete_t) {
        std::cout << "111" << std::endl;
    }
};
struct B {
    virtual ~B() {}
};
struct C : A {
    void operator delete(C* ptr, std::destroying_delete_t) {
        std::cout << "222" << std::endl;
    }
};
struct D : B {
    void operator delete(D* ptr, std::destroying_delete_t) {
        std::cout << "333" << std::endl;
    }
};
int main() {
    A* a = new A;
    delete a; // 输出 111
    C* c = new C;
    delete c; // 输出 222
    B* b = new D;
    delete b; // 输出 333(静态类型为 B,但实际调用 D 的 destroying delete)
    return 0;
}

3、宏

3.1、__VA_OPT__宏

编译器版本:GCC 12

在 C++20 中,__VA_OPT__ 是一个用于支持 可变参数宏(variadic macros)中空变参处理 的新特性。它允许你在宏定义中根据是否存在可变参数来选择性地插入内容。

示例 :根据不同参数执行不同逻辑

可以结合 __VA_OPT__ 来根据是否传入参数做不同的事情。

#define CALL(func, ...) func(__VA_OPT__(__VA_ARGS__))

调用示例:

CALL(foo);              // 展开为 foo()
CALL(bar, 1, 2, 3);     // 展开为 bar(1, 2, 3)

4、属性

4.1、likely与 unlikely

编译器版本:GCC 9

这两个属性用于向编译器提示某个分支的执行概率,帮助其进行更高效的指令调度和分支预测优化。

适用于 if、switch 等控制流语句中的分支判断,尤其在性能敏感的逻辑路径中非常有用。

示例:

int f(int i) {
    switch(i) {
        case 1: [[fallthrough]]; // 显式说明允许 fall-through
        [[likely]] case 2: return 1; // 高概率进入该分支
        [[unlikely]] case 3: return 2; // 极低概率进入该分支
    }
    return 4;
}

4.2、no_unique_address

编译器版本:GCC 9

此属性用于指示某个非静态成员变量可以不占用唯一地址空间,通常用于优化空类型(empty type)成员对象的内存布局。

示例 1:基本用法

#include <iostream>
struct A {}; // 空类型
struct C {};
struct B {
    long long v;
    [[no_unique_address]] C a, b;
};
int main() {
    B b;
    std::cout << &b.v << std::endl; // 输出 v 的地址
    std::cout << &b.a << std::endl; // 地址为 &v + 1
    std::cout << &b.b << std::endl; // 地址为 &v + 2
    std::cout << sizeof(B) << std::endl; // 输出 8
    return 0;
}

示例 2:多个空对象共享地址空间

#include <iostream>
struct A {}; // 空对象
struct B {
    int v;
    [[no_unique_address]] A a, b, c, d, e, f, g;
};
int main() {
    B b;
    std::cout << &b.v << std::endl;
    std::cout << &b.a << std::endl;
    std::cout << &b.b << std::endl;
    std::cout << &b.c << std::endl;
    std::cout << &b.d << std::endl;
    std::cout << &b.e << std::endl;
    std::cout << &b.f << std::endl;
    std::cout << &b.g << std::endl;
    std::cout << sizeof(B) << std::endl; // 输出 8
    return 0;
}

4.3、nodiscard(带消息支持)

编译器版本:GCC 10

C++20 扩展了 [[nodiscard]] 属性,允许为其附加一条自定义警告信息,提醒调用者不要忽略返回值。

示例:

[[nodiscard("返回值不可忽略,请检查错误码")]]
const char* get() {
    return "";
}
int main() {
    get(); // 警告:ignoring return value of ‘const char* get()’, declared with attribute nodiscard: "返回值不可忽略,请检查错误码"
    return 0;
}

5、弃用

C++20 对一些长期存在但容易引发误解或错误使用的语言特性进行了弃用(deprecation)处理。这些特性的使用仍然合法,但鼓励开发者采用更安全、更明确的新方式替代它们。

5.1、Lambda 表达式中 [=]隐式捕获 this

弃用说明:

在 lambda 表达式中使用 [=] 捕获列表时,会隐式地将 this 指针按值捕获,从而允许访问类成员变量。但由于这种行为不直观,容易导致悬空引用或难以察觉的生命周期问题,因此 C++20 中将其标记为弃用

5.2、比较运算符的改进(弃用部分隐式转换)

① 枚举类型的隐式算术转换被弃用

在 C++20 之前,枚举类型可以隐式转换为整型进行比较或算术运算。但在 C++20 中,这类操作已被标记为弃用。

旧写法(弃用):

enum E1 { e };
enum E2 { f };
int main() {
    bool b = e <= 3.7;     // 弃用:e 被隐式转换为 int
    int k = f - e;         // 弃用:f 和 e 被隐式转换为 int
    auto cmp = e <=> f;    // 错误:无法使用 spaceship 运算符
    return 0;
}

推荐做法:

  • 如需进行数值比较,应显式转换为整型:
bool b = static_cast<int>(e) <= 3.7;
int k = static_cast<int>(f) - static_cast<int>(e);
  • 若需要支持 <=> 比较,应为枚举定义合适的运算符重载。

② 数组之间的比较被弃用

数组之间的直接比较(如 ==、!=)在 C++20 中也被标记为弃用,因为其实际比较的是数组首地址,而非数组内容,这容易引起误解。

旧写法(弃用):

int arr1[5];
int arr2[5];
bool same = arr1 == arr2;   // 弃用:比较的是 &arr1[0] == &arr2[0]
auto cmp = arr1 <=> arr2;   // 错误:不支持 spaceship 运算符

推荐做法:

使用标准库函数逐个比较数组内容:

#include <algorithm>
bool same = std::equal(std::begin(arr1), std::end(arr1), std::begin(arr2));

5.3、下标表达式中的逗号操作符被弃用

弃用说明:

在下标表达式中使用逗号操作符(,)来分隔多个表达式的行为,在 C++20 中被标记为弃用。虽然逗号操作符本身并未被弃用,但在数组索引上下文中使用它容易引起混淆。

旧写法(弃用):

int main() {
    int a[3] = {0, 1, 3};
    int tmp1 = a[4, 1];       // tmp1 = a[1] = 1 (只取最后一个表达式)
    int tmp2 = a[10, 1, 2];   // tmp2 = a[2] = 3 (同样只取最后一个)
    return 0;
}

推荐做法:

将逗号操作符从下标表达式中移除,改为单独计算索引值:

int index = (10, 1, 2); // 明确写出逗号表达式的结果
int tmp2 = a[index];    // 明确表示索引

或者直接避免使用逗号表达式:

int tmp1 = a[1];
int tmp2 = a[2];

C++20 在保持语言强大性能的同时,引入了许多实用的新特性和语法改进,提升了代码的可读性、安全性和开发效率。从属性增强到宏优化,从 lambda 捕获规范到弃用易错用法,这些变化体现了 C++ 标准不断演进的方向。

掌握这些新特性,有助于我们编写更现代、更可靠的 C++ 程序,并为未来学习更高版本打下坚实基础。

点击下方关注【Linux教程】,获取编程学习路线、原创项目教程、简历模板、面试题库、AI 知识库、编程交流圈子。


网站公告

今日签到

点亮在社区的每一天
去签到