跨越十年的C++演进系列,分为5篇,本文为第四篇,后续会持续更新C++23~
前3篇如下:
跨越十年的C++演进:C++11新特性全解析
跨越十年的C++演进:C++14新特性全解析
C++20标准是C++语言的第四个正式标准,于2020年12月正式发布。
首先先上C++20特性思维导图:
接下来将从关键字、语法、宏、属性、弃用这5个类目来讲解~
编译器版本:GCC 10
concept 是 C++20 引入的重要特性之一,尤其适用于模板库的设计与开发。其功能类似于 C# 中的泛型约束,但相比而言更为灵活和强大。
concept 允许我们为模板参数定义具有特定条件的类型约束。
示例:数值类型的约束
#include
// 定义一个名为 number 的 concept,用于约束模板参数 T 必须是算术类型
template
concept number = std::is_arithmetic_v;
// 使用该 concept 来限制函数模板的参数类型
template
void func(T t)
{ }
// 调用示例
func(10); // 合法,int 属于算术类型
func(20.0); // 合法,double 属于算术类型
struct A { };
func(A()); // 非法,A 不是算术类型,编译失败
编译器版本:GCC 10
仅使用 concept 还不足以完全发挥其潜力,真正使其强大的是 requires 表达式。通过结合 concept 和 requires,可以对模板参数进行更细粒度的限制,包括检查成员变量、成员函数及其返回值等。
示例:约束类型必须具备某些成员函数或行为
#include
template
concept can_run = requires(T t)
{
std::is_class_v; // 类型 T 必须是一个类或结构体
t(); // 必须支持无参调用(括号运算符重载)
t.run(); // 必须包含 run 成员函数
std::is_same_v; // run 函数返回类型必须为 int
};
// 使用该 concept 的函数模板
template
int func(T t)
{
t();
return t.run(); // 可以直接返回 run() 的结果,因为其返回类型已被限定为 int
}
func(10); // 错误,int 不是 class 或 struct
struct A {
void run() { }
};
func(A()); // 错误,缺少括号运算符重载
struct B {
void operator()() { }
};
func(B()); // 错误,缺少 run 函数
struct C {
void operator()() { }
void run() { }
};
func(C()); // 错误,run 返回类型不是 int
struct D {
int operator()() { }
int run() { return 0; }
};
func(D()); // 正确,满足所有条件
编译器版本:GCC 9
typename 在模板中主要有两个用途:一是作为模板参数声明;二是明确告诉编译器某个嵌套名称是一个类型名。在早期版本中,为了避免歧义,需要频繁使用 typename 来辅助编译器解析。而新版本增强了类型推导能力,使得部分场景下可以省略 typename。
例如,在只能推导出类型的上下文中,可不加 typename。
示例:
// 函数返回类型位于全局作用域,只可能是类型,因此无需 typename
template T::R f(); // 合法
// 作为函数参数时则需要显式指定为类型
template void f(T::R); // 非法,无法推断为类型
template
struct PtrTraits {
using Ptr = T*;
};
template
struct S {
using Ptr = PtrTraits::Ptr; // 合法,在 defining-type-id 上下文中
T::R f(T::P p) {
return static_cast(p); // 合法,在函数体内也可识别为类型
}
auto g() -> S::Ptr; // 合法,尾随返回类型
};
template
void f() {
void (*pf)(T::X); // 合法,pf 是指向函数的指针
void g(T::X); // 非法,T::X 无法被解释为类型
}
编译器版本: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; // 非法,禁止隐式构造
编译器版本:GCC 9
该特性最初在 C++11 中引入,如需详细了解可参考 C++11 的新特性。
① 在 C++20 中,constexpr 的使用范围得到了进一步扩展,新增了对虚函数的支持。其用法与普通函数一致,无需额外说明。
② constexpr 函数中不再允许使用 try-catch 语句块。此限制也适用于构造函数和析构函数中的异常处理逻辑。
编译器版本:GCC 9
char8_t 是为 UTF-8 编码专门设计的新类型。今后,UTF-8 字符字面量将由 char8_t 类型接收,而不再是 char 类型。
目前 GCC 编译器对该特性的支持尚未完全实现,相关内容仍在持续完善中。
编译器版本:GCC 11
consteval 关键字用于定义“立即函数”,这类函数必须在编译期执行完毕,并返回一个编译期常量结果。函数参数也必须是能够在编译期确定的值,且函数内部的所有运算都必须能在编译期完成。
相较于 constexpr,consteval 对函数的限制更加严格。constexpr 函数会根据传入参数是否为常量表达式自动决定是在编译期还是运行期执行;而 consteval 函数则强制要求所有调用都必须发生在编译期。
示例代码:
#include
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;
}
编译器版本:GCC 10
编译选项:-fcoroutines
协程三大关键字:co_await、co_yield 和 co_return。
(先了解基本语法,后文将详细解释)
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
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 test_await_print_func()
{
std::this_thread::sleep_for(1000ms);
std::cout << __func__ << std::endl;
return FuncAwaiter{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();
}
总结:
一个合法的 awaiter 类型必须实现以下三个接口函数:
协程函数的返回类型必须包含一个名为 promise_type 的嵌套类型。这个 promise_type 负责管理协程的状态和返回值。
编译器会自动调用 promise_type::get_return_object() 来获取协程函数的返回值(通常是 generator 类型),用户无需手动编写 return 语句。
通常情况下,generator 类型需要保存协程的句柄,以便外部程序控制协程的执行流程。
编译器版本: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;
}
编译器版本: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 不是常量表达式
};
编译器版本:GCC 8
在 C++20 中,允许对 .* 表达式中的第二个操作数进行更灵活的处理。特别是当该操作数是一个指向带有 & 限定符的成员函数指针时,只有在其具有 const 限定的情况下才是合法的。
示例:
struct S {
void foo() const& { }
};
void f()
{
S{}.foo(); // 合法,调用 const& 成员函数
(S{}.*&S::foo)(); // C++20 起支持这种语法
}
编译器版本:GCC 8
lambda 表达式现在可以显式地按值捕获 this 指针,这使得 lambda 在捕获对象状态时更加清晰明确。
示例:
struct S
{
int value;
void print()
{
auto f = [=, this]() {
this->value++;
};
}
};
上述代码中,[=, this] 表示以值方式捕获所有外部变量,并且显式捕获 this 指针。
编译器版本: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
}
编译器版本:GCC 8
C++20 支持在 lambda 表达式中使用模板参数,从而实现泛型 lambda。这一特性极大增强了 lambda 的灵活性和通用性。
示例 1:
int a;
auto f = [&a](const T &m) {
a += m;
};
f(10); // 正确,T 推导为 int
示例 2:
template
int func(int t)
{
return t * t;
}
int f()
{
return func(20); // 使用 lambda 类型作为模板参数
}
示例 3:
using A = decltype([] {});
void func(A *) { }
func(nullptr);
template
using B = decltype([] {});
void f1(B *) { }
template
void f2(B *) { }
f1(nullptr); // 合法
f2(nullptr); // 合法
编译器版本:GCC 8
C++20 允许在变量声明时省略模板参数,编译器将根据构造函数参数自动推导出实际类型。
示例:
vector v{vector{1, 2}}; // 合法,v 被推导为 vector>
tuple t{tuple{1, 2}}; // 合法,t 被推导为 tuple
编译器版本:GCC 8
本节内容较为复杂,涉及 lambda 表达式捕获机制的改进。由于篇幅限制,此处不再详细展开。
如需深入了解,请参考提案文档:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html
编译器版本:GCC 9
ADL(Argument-Dependent Lookup)是 C++ 中的一种机制,用于自动解析调用函数的位置,简化代码编写。C++20 扩展了这一机制,使得它也可以应用于模板函数的推导。
示例:
int h;
void g();
namespace N {
struct A {};
template int f(T);
template int g(T);
template int h(T); // 注意这里的 h 是一个模板函数
}
int x = f(N::A()); // 正确,调用了 N::f
int y = g(N::A()); // 正确,调用了 N::g
int z = h(N::A()); // 错误,因为全局命名空间中的 h 被认为是一个变量而非模板函数
由于篇幅限制,关于 operator<=> 的详细讨论请参考官方文档:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0515r3.pdf
编译器版本: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 进行操作
}
编译器版本:GCC 9
C++20 允许获取 lambda 表达式的类型,并创建该类型的对象,即使这个 lambda 没有捕获任何外部变量。
示例:
#include
#include
此特性已在 GCC 和 MSVC 编译器中实现,但在其他编译器中可能未完全支持。它允许在模板类内部忽略访问权限来访问另一个类的嵌套类。
示例:
class A {
struct impl1 { int value; };
template class impl2 { T value; };
class impl3 { int value; };
};
struct B {
A::impl1 t; // 错误:'struct A::impl1' 在此上下文中为私有
};
template
struct trait {
A::impl1 t; // 正确
A::impl2 t2; // 正确
void func() {
A::impl1 tmp; // 正确
tmp.value = 10;// 正确
t2.value = 20; // 正确
A::impl3 t3; // 正确
t3.value = 30; // 正确
}
};
int main() {
trait a;
a.t.value = 10; // 正确
a.t2.value = 20; // 错误:'int A::impl2::value' 在此上下文中为私有
return 0;
}
编译器版本:GCC 9
当仅需获取 constexpr 函数的返回值类型时,无需实例化整个函数,只需推导其返回类型即可。
示例:
template
constexpr int f() { return T::value; }
// 此处仅推导 f() 的返回值类型
template
void g(decltype(B ? f() : 0)) { }
template void g(...) { }
// 因为需要实际获取 int 类型的数据,所以必须实例化 f()
template void h(decltype(int{B ? f() : 0})) { }
template void h(...) { }
void x() {
g(0); // OK,因为不需要实例化 f()
h(0); // 错误,即使 B 为 false,也需要实例化 f()
}
编译器版本:GCC 9
C++20 扩展了包扩展的应用场景,现在可以在 lambda 初始化捕获时使用包扩展。
示例:
#include
template
auto invoke1(F f, Args... args) {
return [f, args...]() -> decltype(auto) {
return std::invoke(f, args...);
};
}
template
auto invoke2(F f, Args... args) {
return [f=std::move(f), ...args=std::move(args)]() -> decltype(auto) {
return std::invoke(f, args...);
};
}
template
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);
};
}
编译器版本:GCC 8
C++20 放宽了结构化绑定的限制,并允许自定义查找规则以适应更复杂的绑定需求。
自定义条件包括:
示例 1:
#include
#include
struct A {
int a;
int b;
};
struct X : private A {
std::string value1;
std::string value2;
};
template
auto& get(X &x) {
if constexpr (N == 0)
return x.value2;
}
namespace std {
template<>
class tuple_size : public std::integral_constant {};
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
#include
struct A {
int a;
int b;
};
struct X : protected A {
std::string value1;
std::string value2;
template
auto& get() {
if constexpr (N == 0)
return value1;
else if constexpr (N == 1)
return a;
}
};
namespace std {
template<>
class tuple_size : public std::integral_constant {};
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;
}
编译器版本:GCC 8
在 C++20 中,允许不通过类内部的 begin() 和 end() 成员函数来实现基于范围的 for 循环。现在可以将这两个函数实现在类外部,依然可以被正确识别。
示例:
#include
struct X {
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
};
int* begin(X& x) {
return reinterpret_cast(&x);
}
int* end(X& x) {
return reinterpret_cast(&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;
}
编译器版本:GCC 9
C++20 允许使用类类型作为非类型模板参数,前提是该类的所有操作可以在编译期完成,并且满足特定条件(如支持常量比较等)。
#include
struct A {
int value;
// operator== 必须是 constexpr
constexpr bool operator==(const A& v) const {
return value == v.value;
}
};
template
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::value << std::endl; // 输出 false
std::cout << Equal::value << std::endl; // 输出 true
return 0;
}
当使用类类型作为非类型模板参数时,如果两个对象逻辑上相等(即 <=> 比较结果为 0),那么它们实例化的模板也应共享相同的地址。
#include
template
int Value;
struct A {
int value;
};
int main() {
static constexpr A a{10}, b{20}, c{10};
std::cout << std::boolalpha;
std::cout << (&Value == &Value) << std::endl; // false
std::cout << (&Value == &Value) << std::endl; // true
return 0;
}
由于模板参数必须在编译期求值,因此类中的成员函数用于模板参数计算时,必须标记为 constexpr。
C++20 支持从字符串字面量等自动推导模板参数。
#include
template
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
MyArray(const _Tp (&str)[N]) -> MyArray<_Tp, N - 1>;
template
using CharArray = MyArray;
// 旧写法需要显式指定大小
template Str>
struct A {};
using hello_A = A<5, "hello">;
// 新写法可自动推导
template
struct B {};
using hello_B = B<"hello">;
结合上述特性,可以实现基于字符串字面量的模板参数化处理:
template
auto operator""_udl();
"hello"_udl; // 等价于 operator""_udl<"hello">()
强结构可比较性的定义:对于任意类型 T,若 const T 的 glvalue 对象 x,使得 x <=> x 返回 std::strong_ordering 或 std::strong_equality,并且不调用用户定义的三向比较运算符或结构化比较函数,则该类型具有强结构可比较性。
编译器版本:GCC 9
在 C++20 中,禁止使用用户显式声明的构造函数(即使为 default 或 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}; // 旧版本可能通过编译(错误)
}
struct X {
int i;
X() = default;
};
struct Y {
int i;
Y();
};
Y::Y() = default;
int main() {
X x{4}; // 旧版本可能通过编译(错误)
Y y{4}; // 旧版本可能报错(不一致)
}
如果类中显式声明了除拷贝/移动构造函数以外的其他构造函数,则该类不能使用聚合初始化。
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
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 list);
};
int main() {
X x{4}; // 错误:无匹配构造函数
Y y{4}; // 错误:无匹配构造函数
A a{5}; // 正确
B b{5}; // 正确
C c{6}; // 正确:支持 initializer_list 构造函数
return 0;
}
编译器版本:GCC 9
C++20 引入了嵌套内联命名空间的新语法,使得在定义多个层级的命名空间时更加简洁,并且可以更灵活地控制符号的可见性。
#include
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;
}
#include
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;
}
编译器版本:GCC 10
C++20 支持将 auto 和 concept 结合使用,从而简化模板约束的写法,使代码更具表现力和可读性。
#include
struct Compare {
// 使用 auto 替代模板类型,自动推导参数
bool operator()(const auto& t1, const auto& t2) const {
return t1 < t2;
}
};
template
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;
}
编译器版本:GCC 10
C++20 允许在 constexpr 上下文中使用 dynamic_cast 和 typeid,前提是它们的操作对象是已知的静态类型或具有多态性的对象。
#include
#include
struct Base {
virtual ~Base() = default;
};
struct Derived : Base {};
constexpr bool test_dynamic_cast() {
Derived d;
Base* b = &d;
return dynamic_cast(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");
编译器版本:GCC 10
C++20 扩展了聚合初始化语法,允许使用圆括号 ( ) 来代替花括号 { },只要目标类型是聚合类型。
#include
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;
}
编译器版本:GCC 11
C++20 支持在使用 new 创建数组时省略大小,由编译器根据初始化列表自动推导。
#include
#include
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;
}
编译器版本:GCC 10
C++20 支持 UTF-16 和 UTF-32 编码的字符串字面量,分别使用前缀 u 和 U。
#include
int main() {
std::u16string str1 = u"aaaaaa"; // UTF-16 字符串
std::u32string str2 = U"bbbbbb"; // UTF-32 字符串
return 0;
}
编译器版本:GCC 10
C++20 允许将数组作为实参传递给接受未知边界数组的形参。这种特性对于泛型编程非常有用。
template
static void func(T (&arr)[]) {
// 接收任意大小的数组
}
template
static void func(T (&&arr)[]) {
// 接收临时数组
}
int main() {
int a[3];
int b[6];
func(a); // OK
func(b); // OK
func({1, 2, 3, 4}); // OK
func({1.0, 2, 3, 4, 8.0}); // OK
return 0;
}
编译器版本:GCC 8
C++20 支持通过聚合初始化的方式推导类模板参数类型,提升了模板编程的灵活性。
template
struct S {
T x;
T y;
};
template
struct C {
S s;
T t;
};
template
struct D {
S 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
D d1 = {1, 2}; // error: deduction failed
D d2 = {1, 2, 3}; // OK: 推导为 D
template
struct I {
using type = T;
};
template
struct E {
typename I::type i;
T t;
};
E e1 = {1, 2}; // OK: 推导为 E
编译器版本:GCC 11
C++20 引入了一项优化:在某些情况下,函数中返回局部变量时会自动使用移动语义(move)而不是复制(copy),即使没有显式使用 std::move。
以下两种情况会触发隐式 move:
✅ 隐式可移动实体定义:
是局部变量;
没有被 const 修饰;
不是数组;
不是通过花括号 {} 初始化的聚合类型;
没有绑定到引用;
没有被取地址(&)操作符使用过。
#include
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;
}
编译器版本:GCC 10
C++20 支持对按值传递参数的比较运算符使用 = default,用于自动生成默认实现。
struct C {
friend bool operator==(C, C) = default; // 合法!C++20 起支持
};
编译器版本:GCC 10
当两个非类型模板参数的值被认为是“等效”的时候,它们可以被视为相同的模板实参。这在模板特化、别名推导等场景中非常重要。
类型 |
判断条件 |
整型 |
值相同 |
浮点型 |
值相同 |
std::nullptr_t |
都为 nullptr |
枚举类型 |
枚举值相同 |
指针类型 |
指向同一对象或函数 |
成员指针类型 |
指向同一个类的同一成员,或者都为空 |
引用类型 |
引用同一个对象或函数 |
数组类型 |
所有元素都满足等效条件 |
共用体类型 |
没有活动成员,或具有相同的活动成员且其值等效 |
类类型 |
所有直接子对象和引用成员满足等效条件 |
编译器版本:GCC 9
C++20 引入了新的 operator delete 形式:destroying operator delete,它允许在析构对象的同时控制内存释放行为。
void operator delete(T* ptr, std::destroying_delete_t);
#include
#include // 包含 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;
}
#include
#include
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、宏
编译器版本: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、属性
编译器版本: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;
}
编译器版本:GCC 9
此属性用于指示某个非静态成员变量可以不占用唯一地址空间,通常用于优化空类型(empty type)成员对象的内存布局。
#include
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
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;
}
编译器版本: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;
}
在 lambda 表达式中使用 [=] 捕获列表时,会隐式地将 this 指针按值捕获,从而允许访问类成员变量。但由于这种行为不直观,容易导致悬空引用或难以察觉的生命周期问题,因此 C++20 中将其标记为弃用。
在 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(e) <= 3.7;
int k = static_cast(f) - static_cast(e);
数组之间的直接比较(如 ==、!=)在 C++20 中也被标记为弃用,因为其实际比较的是数组首地址,而非数组内容,这容易引起误解。
int arr1[5];
int arr2[5];
bool same = arr1 == arr2; // 弃用:比较的是 &arr1[0] == &arr2[0]
auto cmp = arr1 <=> arr2; // 错误:不支持 spaceship 运算符
使用标准库函数逐个比较数组内容:
#include
bool same = std::equal(std::begin(arr1), std::end(arr1), std::begin(arr2));
在下标表达式中使用逗号操作符(,)来分隔多个表达式的行为,在 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 知识库、编程交流圈子。