C++20

C++20，是继C++17之后的C++编程语言的ISO/IEC标准修订版的名称。2020年2月，该标准在布拉格的会议上由WG21进行了技术定稿。同年9月4日草案获得批准后，C++20同年12月正式发布。相比 C++17，C++20引入了新的语言特性，如概念、模块、操作符“&lt;=&gt;”、协程、指定初始化、新标准属性等。C++20库标准还加入了范围、特性测试宏和位操作等。
特性改动.
新增语言特性.
功能特性测试.
自C++11之后标准引入了大量的C++语言、库特性，在20标准前为了区分这些特性是否生效只能判断C++标准。20标准为这些语言和程序库的功能特性定义了一组预处理器宏，使之成为检测这些功能特性是否存在的一种简单且可移植的方式。测试宏展开会得到该语言、库特性添加到标准草案中的年份和月份，如果该特性有显著变更，宏展开的时间也为更新。
__has_cpp_attribute( 属性记号 )
宏函数，用以检测属性是否支持，如：
用以检测当前某个语言功能特性是否支持，单个宏，如：
用以检测当前某个标准库特性是否支持，单个宏，不由编译器预定义，由&lt;version&gt;头文件定义：
三路比较和比较操作符的默认.
新增三路比较运算符，又称spaceship operator，其形式为：
左操作数 &lt;=&gt; 右操作数
表达式返回一个对象，使得
三路比较操作符会作为&lt; &lt;= &gt; &gt;=四个操作符的重写候选，若决议选择了带参数顺序的operator&lt;=&gt;，则对于操作如x @ y，执行 x &lt;=&gt; y @ 0，对于不带参数顺序的执行 0 @ x &lt;=&gt; y。
新增可以将比较操作符显式预置=default来要求编译器为某个类生成对应比较，比如：
struct Point
 int x;
 int y;
 auto operator&lt;=&gt;(const Point&amp;) const = default;
聚合体指派初始化.
聚合体初始化的语法糖，在c++11的聚合体初始化基础上，增加了可以指派具体值的语法：
struct U {
 int a;
 float b;
U u1{ 1, 2.0 };
U u2{ .a = 1, .b = 2.0 };
范围for中的初始化语句和初始化器.
17标准中给if和switch语句加了初始化语句，20标准则给基于范围的for加了初始化语句：
std::initializer_list&lt;int&gt; il{ 1, 2, 3 };
for (size_t index{ 0 }; auto&amp; i : il) {
 std::cout « std::format("index {} value is {}", index++, i) « std::endl;
UTF8字符基础类型char8_t.
新增了基础类型char8_t用以表示UTF8字符，与11标准中的char16_t、char32_t一样同为语言关键字，char8_t的出现主要是为了和旧有的char区分，专门用于表示utf8字符。相对应的标准库`&lt;string&gt;`增加了std::u8string的别名，以下来自gcc13：
namespace std _GLIBCXX_VISIBILITY(default)
_GLIBCXX_BEGIN_NAMESPACE_VERSION
 namespace pmr {
 template&lt;typename _CharT, typename _Traits = char_traits&lt;_CharT»
 using basic_string = std::basic_string&lt;_CharT, _Traits,
 polymorphic_allocator&lt;_CharT»;
 using string = basic_string&lt;char&gt;;
 using u8string = basic_string&lt;char8_t&gt;;
 using u16string = basic_string&lt;char16_t&gt;;
 using u32string = basic_string&lt;char32_t&gt;;
 using wstring = basic_string&lt;wchar_t&gt;;
 } // namespace pmr
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace std
新属性.
no_unique_address.
该属性适用于非位域非静态数据成员，指示编译器可以优化当前成员使其与其他非静态数据成员重叠，减少内存占用。如果该成员为空类型（不具有数据成员），则编译器优化为不占空间；如果该成员不为空，则其尾随填充空间可被其他数据成员占用。两个场景例子：
struct Empty {};
struct WithEmpty {
 int32_t x; // 4字节
 Empty e; // 1字节（填充至对齐）
}; // 总大小8（4+1+3填充）
struct WithEmptyAttri {
 int32_t x; // 4字节
 no_unique_address Empty e; // 优化为不占空间
}; // 总大小4字节
struct NonEmpty {
 int32_t x; // 4 字节
 no_unique_address char y; // 1字节，无法优化，仍需对齐，*注意这里指定了no_unique_address
 // 3 字节填充
}; // 总大小8字节
struct WithNonEmpty {
 NonEmpty ne; // 8 字节，未指定no_unique_address，z不可复用ne空间
 char z; // 1字节, 需对齐
 // 3 字节填充
}; // 总大小12字节
struct Optimized {
 no_unique_address NonEmpty ne; // 8 字节
 char z; // 可以复用 ne 的尾随填充
int main() {
 static_assert(sizeof(WithEmpty) == 8);
 static_assert(sizeof(WithEmptyAttri) == 4);
 static_assert(sizeof(NonEmpty) == 8);
 static_assert(sizeof(WithNonEmpty) == 12);
 static_assert(sizeof(Optimized) == 8);
 WithNonEmpty wn;
 assert(&amp;wn.z == &amp;wn.ne.y + 4);
 Optimized o;
 assert(&amp;o.z == &amp;o.ne.y + 1);
 return 0;
likely与unlikely.
现代cpu有指令预取和分支预测功能，在gcc以往也有__builtin_expect，相对应的20标准新增了likely和unlikely属性作为c++标准的分支预测优化属性，用于给程序员协助编译器完成分支预测。
lambda更新.
lambda显式模板形参.
可以显式声明模板形参用以表示当前为泛型lambda，如：
auto print = []&lt;typename T&gt;(const T &amp;t) { /*...*/ };
lambda捕获参数包.
template &lt;typename... Args&gt;
auto FactoryByValue(Args&amp;&amp;... args) {
 return [... args = std::forward&lt;Args&gt;(args)]() {
 ((std::cout « "Value:" « args « " Address:" « &amp;args « std::endl), ...);
template &lt;typename... Args&gt;
auto FactoryByRef(Args&amp;&amp;... args) {
 return [&amp;... args = std::forward&lt;Args&gt;(args)]() {
 ((std::cout « "Value:" « args « " Address:" « &amp;args « std::endl), ...);
以上代码中分别按值和按引用捕获了参数包，并通过一元右折叠(c++17功能)打开了参数包。
隐式按值捕获this的弃用.
20起须显式声明this的捕获方式，如下代码将被编译器告警：
struct A {
 void Test() {
 auto f = [=]() { cout « this « endl; };
};
typename 关键字简化.
模板声明中typename的使用被简化，以下场景中无须再使用typename：
struct Data {
 using ValueType = int32_t;
 using ValuePointer = int32_t*;
template &lt;typename T&gt;
struct Foo {
 using Type = T::ValueType;
 typedef T::ValuePointer Pointer;
 T::ValueType val_;
 T::ValueType Get();
 auto Get(size_t index) -&gt; T::ValuePointer;
 template &lt;typename U = T::ValuePointer&gt;
 void Set(U u);
consteval与constinit.
consteval.
新增关键字consteval，只可用于修饰函数，consteval修饰的函数必须是编译期执行：
consteval int sqr(int x) {
 return x * x;
constinit.
constinit关键字仅可用于变量，表明该变量拥有静态初始化，即该变量的初值为编译期常量，亦即const initialize，仅可用于静态存储期和线程存储期的变量。
constinit const char* c = f(true); // OK
// constinit const char* d = f(false); // 错误
constexpr 改动.
constexpr int Divide(int a, int b) {
 if (b == 0)
 throw std::runtime_error("Division by zero");
 return a / b;
constinit int i = Divide(1, 2); // 正常编译期常量
//constinit int j = Divide(1, 0); // 编译报错，编译期不允许出现异常
constexpr int* CreateArray(int size) {
 int* arr = new int[size];
 return arr;
constexpr void ReleaseArray(int* p) {
 delete[] p;
constexpr int Sum(int n) {
 int* p = CreateArray(n);
 std::iota(p, p + n, 1); // constexpr since C++20
 auto t = std::accumulate(p, p + n, 0); // constexpr since C++20
 ReleaseArray(p);
 return t;
constinit int i = Sum(10); // 正常编译期计算累计值55
// constinit int* p = CreateArray(100); // 编译出错，编译期内存不能出现在运行期
int* ap = CreateArray(100); // constexpr函数可以是运行期函数
class Parent {
public:
 constexpr virtual ~Parent() = default;
 constexpr virtual int GetID() {
 return 0;
};
class Child : public Parent {
public:
 constexpr virtual ~Child() = default;
 constexpr int GetID() override {
 return 1;
};
constinit int i = Parent().GetID();
constinit int j = Child().GetID();
union Test {
 int i;
 int j;
constexpr int Foo()
 Test t{.i = 1};
 t.j = 2;
 return t.j;
constinit int i = Foo();
constexpr int Foo() {
 std::vector&lt;int&gt; v;
 v.emplace_back(9);
 v.emplace_back(99);
 v.emplace_back(123); // vector::emplace_back 20起为constexpr
 std::sort(v.begin(), v.end()); // std::sort 20起为constexpr
 // std::remove_if 20起为constexpr
 const auto erase_start =
 std::remove_if(v.begin(), v.end(), [](auto&amp; i) { return i &lt; 100; } );
 v.erase(erase_start, v.end()); // vector::erase 20起为constexpr
 std::string n = "Joshua";
 return v.size() + n.size(); // 两个size 20起都为constexpr
constinit int i = Foo();
有符号整数必须以补码表示.
20标准起有符号整数必须以补码表示，N位有符号整数的取值范围也就固定为-(2N-1-1)到2N-1-1。
移位运算符的行为变化.
规范了左移、右移操作行为：
允许使用圆括号进行聚合初始化.
struct U {
 int a;
 float b;
int main()
 U u(1,2);
 std::cout « u.a « " " « u.b « std::endl;
缩略函数模板.
函数模板可以省略`template&lt;&gt;`声明进行简写：
void f1(auto t) {} // =&gt; template&lt;class T&gt; void f1(T);
void f2(const auto&amp; t, auto u) {} // =&gt; template&lt;class T, class U&gt; void f2(const T&amp;, U)
void f3(std::integral auto) {} // =&gt; template&lt;std::integral T&gt; void f3(T)
void f4(std::integral auto&amp;...) {} // =&gt; template&lt;std::integral... Ts&gt; void f4(Ts&amp;...)
数组new 可推导数组长度.
int a[]{ 1, 2, 3, 4 }; // 11标准后编译通过
int* pa = new int[]{ 1, 2, 3, 4 }; // 20标准前无法通过编译，20后可以
标准规定在有列表初始化的情况下，可以通过列表个数来确定数组大小。