三年一度的C++标准文章这回在不到两年的时间点就开始了(请想象自嘲口吻),这次是一个持续更新形式,主要由于本就是利用业余时间写的,近两年家里又多了个小淘气,闲暇时间就越发少了,尽早更新完吧。这次看到中文维基百科上C++标准的条目内容有所缺失,所以同时还会持续更新这个页面维基百科 C++20 ,当然维基百科上不能像这篇文章里一样这么多废话。😂
新的语言特性
特性测试宏
或称功能特性测试,在c++标准中增加了预处理宏用以检测当前的c++功能支持情况,以往这些预处理都是根据编译器版本宏来控制,现在标准中增加了这些宏。
这些宏包含:
属性检测宏__has_cpp_attribute,形式是宏函数内传递属性token:
1 2 3 #if __has_cpp_attribute(nodiscard) > 201603L #pragma message("nodiscard version is c++20" ) #endif
语言功能特性宏,与属性函数宏不一样,功能宏是单个宏定义:
1 2 3 #if __cpp_concepts >= 201907L #pragma message("support concepts" ) #endif
标准库功能特性宏,同样是单个宏定义,但这些宏不是预定义,由<version>
头文件定义:
1 2 3 4 5 #include <version> #ifdef __cpp_lib_bitops #pragma message("support bitops" ) #endif
由以上可见这些宏定义都定义为了标准实现版本,可以根据其时间版本来确定具体的功能。完整的所有宏列表见cppreference
三路比较和比较操作符的默认
20标准新增三路比较操作符,其操作符为<=>,由于像个宇宙飞船也称为spaceship operator。以往标准中比较操作符有==, !=, <, >, <=, >=,可能标准委员会也厌烦了把六个都定义一遍,也就有了三路比较。三路比较返回一个对象,使得
如果a<b, (a <=> b) < 0
如果a>b, (a <=> b) > 0
如果a和b相等/等价,那么(a<=>b) ==0
具体来说三路运算符可以返回三种类型,std::strong_ordering、std::weak_ordering、std::partial_ordering,分别代表强、弱、部分比较类型,这里面细节很多,建议还是用到再来查手册,只要记住三路运算符逻辑即可:
1 2 3 4 5 6 7 8 9 10 11 double foo = -0.0 ;double bar = 0.0 ;auto res = foo <=> bar;if (res < 0 ) std::cout << "-0 小于 0" ; else if (res > 0 ) std::cout << "-0 大于 0" ; else if (res == 0 ) std::cout << "-0 与 0 相等" ; else std::cout << "-0 与 0 无序" ;
同时20标准新增特性,可以对七个比较运算符设置default,而默认比较行为自然是调用相应成员对象的相应操作符。而之所以说三路运算符可以减少代码量,源于以下两个行为:
如果类C没有显示声明任何名为operator==的成员或友元,编译器会负责对每一个定义为default的<=>操作符定义一个相同实现但是返回值为bool的operator==
三路操作符会作为其他比较操作符<、<=、>、>=的重写候选
也就是说如果没有特殊需求,只要定义一个=default的operator<=>即可省去定义其他比较操作符的麻烦。以gcc13 vector代码为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #if __cpp_lib_three_way_comparison template <typename _Tp, typename _Alloc> _GLIBCXX20_CONSTEXPR inline __detail::__synth3way_t <_Tp> operator <=>(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y) { return std::lexicographical_compare_three_way (__x.begin (), __x.end (), __y.begin (), __y.end (), __detail::__synth3way); } #else template <typename _Tp, typename _Alloc> inline bool operator <(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y) { return std::lexicographical_compare (__x.begin (), __x.end (), __y.begin (), __y.end ()); } template <typename _Tp, typename _Alloc> inline bool operator !=(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y) { return !(__x == __y); } template <typename _Tp, typename _Alloc> inline bool operator >(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y) { return __y < __x; } template <typename _Tp, typename _Alloc> inline bool operator <=(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y) { return !(__y < __x); } template <typename _Tp, typename _Alloc> inline bool operator >=(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y) { return !(__x < __y); } #endif
聚合体指派初始化
聚合体初始化的语法糖,在c++11的聚合体初始化基础上,增加了可以指派具体值的语法:
1 2 3 4 5 6 7 struct U { int a; float b; }; U u1{ 1 , 2.0 }; U u2{ .a = 1 , .b = 2.0 };
以上代码中u1与u2等效,指派初始化仍然是聚合初始化,所以他符合聚合初始化规则,但需要注意以下几点:
指派顺序必须按照声明顺序,如果其中元素可以被统一初始化,可以不指派所有元素,如:
1 2 3 4 5 6 7 struct U { int a; string b; float c; short d; }; U u{ .a = 1 , .c = 2.0 };
可以用于初始化联合体,但只能为联合的一个成员使用指派初始化,可以对匿名联合体进行指派:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 union U { int a; float b; short c; }; struct AU { union { int a; float b; }; string c; }; U u{ .b = 10 }; AU au{ .b = 1 , .c = "asd" };
C语言从c99标准开始就支持了指派初始化,但是C++20开始的指派初始化与C的规则不同,具体来说乱序指派、嵌套指派、与常规初始化混用在c99标准后是可以的,但是在c++20标准中不允许:
1 2 3 4 5 6 7 8 9 10 11 struct A { int x, y; }; struct B { struct A a; }; struct A a = { .y = 1 , .x = 2 }; int arr[3 ] = { [1 ] = 5 }; struct B b = { .a.x = 0 }; struct A a = { .x = 1 , 2 };
范围for中的初始化语句和初始化器
17标准中给if和switch语句加了初始化语句,20标准则给基于范围的for加了初始化语句:
1 2 3 4 std::initializer_list<int > il{ 1 , 2 , 3 }; for (size_t index{ 0 }; auto & i : il) { std::cout << std::format("index {} value is {}" , index++, i) << std::endl; }
该特性为了解决以下这种场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class UDC {public : UDC () : items_{ 1 , 2 , 3 , 4 } {} const std::vector<int >& ItemsRef () { return items_; } private : std::vector<int > items_; }; UDC foo () { return {}; } for (auto & i : foo ().ItemsRef ()) { std::cout << std::format("current is {} " , i) << std::endl; }
以上代码在c++20标准中会导致最后空悬引用,但在23标准中有所改善,23标准是后话了(比如这里使用23标准的clang19编译后就能正常输出 见godbolt-compiler-explorer )。问题在于临时对象的生命周期问题,那这里就有了初始化语句的变通方式:
1 2 3 for (auto obj = foo (); auto & i : obj.ItemsRef ()) { std::cout << std::format("current is {} " , i) << std::endl; }
UTF8字符基础类型char8_t
新增了基础类型char8_t用以表示UTF8字符,与11标准中的char16_t、char32_t一样同为语言关键字,char8_t的出现主要是为了和旧有的char区分,专门用于表示utf8字符。相对应的标准库<string>
增加了std::u8string的别名,以下来自gcc13:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #if __cplusplus >= 201703L && _GLIBCXX_USE_CXX11_ABI #include <bits/memory_resource.h> namespace std _GLIBCXX_VISIBILITY(default ){ _GLIBCXX_BEGIN_NAMESPACE_VERSION namespace pmr { template <typename _CharT, typename _Traits = char_traits<_CharT>> using basic_string = std::basic_string<_CharT, _Traits, polymorphic_allocator<_CharT>>; using string = basic_string<char >; #ifdef _GLIBCXX_USE_CHAR8_T using u8string = basic_string<char8_t >; #endif using u16string = basic_string<char16_t >; using u32string = basic_string<char32_t >; using wstring = basic_string<wchar_t >; } _GLIBCXX_END_NAMESPACE_VERSION } #endif
新属性
no_unique_address
该属性适用于非位域非静态数据成员,指示编译器可以优化当前成员使其与其他非静态数据成员重叠,减少内存占用。如果该成员为空类型(不具有数据成员),则编译器优化为不占空间;如果该成员不为空,则其尾随填充空间可被其他数据成员占用。两个场景例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct Empty {};struct WithEmpty { int32_t x; Empty e; }; struct WithEmptyAttri { int32_t x; [[no_unique_address]] Empty e; }; struct NonEmpty { int32_t x; [[no_unique_address]] char y; }; struct WithNonEmpty { NonEmpty ne; char z; }; struct Optimized { [[no_unique_address]] NonEmpty ne; char z; }; 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 (&wn.z == &wn.ne.y + 4 ); Optimized o; assert (&o.z == &o.ne.y + 1 ); return 0 ; }
当然这个属性的具体实现是完全依赖编译器的,空类型的优化上gcc和clang表现都一致;但对于类型不为空时,以上代码在gcc14中NonEmpty内如果不给y指定no_unique_address
,最后Optimized
的大小仍然会是12,见godbolt ,只有y也指定了才能优化。
到clang里,无论我如何写属性都无法将Optimized
的大小减为8。
msvc当前版本就更有趣了,当前最新版本msvc 19.40只能使用[[msvc::no_unique_address]]
,然后同样的无法通过这个属性来缩减这个Optimized的大小。
likely与unlikely
现代cpu有指令预取和分支预测功能,在gcc以往也有__builtin_expect,相对应的20标准新增了likely和unlikely属性作为c++标准的分支预测优化属性,用于给程序员协助编译器完成分支预测。
以这个例子 来看,生成的汇编代码里把unlikely的代码放到了远离主干的地方;没有加likely属性的版本则没有这个优化,按照代码顺序生成了汇编。注意这个优化在gcc中需要开O1及以上的优化级别才能有。
lambda更新
lambda显式模板形参
C++14标准中增加了泛型lamda,但彼时标准只允许将形参声明为auto,20标准则明确了可以显式声明模板形参用以表示当前为泛型lambda。如:
1 auto print = []<typename T>(const T &t) { };
有趣的是以上<typename>
的写法在gcc开14标准时是不会报错的,见godbolt_gcc_14.2_c++14 ,而clang还会报警称 explicit template parameter list for lambdas is a C++20 extension
,见godbolt_clang_20.1_c++14 。
lambda捕获参数包
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename ... Args>auto FactoryByValue (Args&&... args) { return [... args = std::forward<Args>(args)]() { ((std::cout << "Value:" << args << " Address:" << &args << std::endl), ...); }; } template <typename ... Args>auto FactoryByRef (Args&&... args) { return [&... args = std::forward<Args>(args)]() { ((std::cout << "Value:" << args << " Address:" << &args << std::endl), ...); }; }
以上代码中分别按值和按引用捕获了参数包,并通过一元右折叠(c++17功能)打开了参数包,该特性的加入主要是考虑到元编程中的需要,示例代码见godbolt 。
隐式按值捕获this的弃用
C++20开始以下代码编译会得到一串警告implicit capture of ‘this’ via ‘[=]’ is deprecated in C++20
:
1 2 3 4 5 struct A { void Test () { auto f = [=]() { cout << this << endl; }; } };
理由比较简单,以往lambda编写时不清晰的表明当前是按值还是按引用捕获的this,程序员总是会混淆当前对this的捕获形式,从而造成指针空悬一类的惨案。17标准又新增了可以复制捕获*this
,所以现在推荐明确显式表明this
的捕获方式:
1 2 3 4 5 struct A { void Test () { auto f = [=, this ]() { cout << this << endl; }; } };
typename 关键字简化
以往标准中模板类型嵌套声明时,必须加上typename,20标准中做了简化,部分嵌套场景编译器可以识别为类型的可以无须typename:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct Data { using ValueType = int32_t ; using ValuePointer = int32_t *; }; template <typename T>struct Foo { using Type = T::ValueType; typedef T::ValuePointer Pointer; T::ValueType val_; T::ValueType Get () ; auto Get (size_t index) -> T::ValuePointer ; template <typename U = T::ValuePointer> void Set (U u); };
以上六处涉及嵌套类型的代码在17标准中都将通不过编译(godbolt ),但在20标准是可以的(godbolt )。另外需要注意的是函数中的部分场景仍然需要typename
:
1 2 3 4 5 6 7 8 9 10 11 template <typename T>T::ValueType Foo<T>::Get () { using Pointer = T::Valuepointer; typedef typename T::ValuePointer Pointer_; typename T::ValuePointer p; auto lam = [](T::ValueType) {}; return static_cast <T::ValueType>(0 ); }
编译见godbolt 。
consteval与constinit
consteval
11标准中加入了constexpr
关键字,当constexpr
修饰函数时表明该函数在一定条件下其返回值是编译器计算的,但这并不是强制的,constexpr
修饰的函数仍然可以是运行期计算的:
1 2 3 4 5 6 7 8 9 10 11 12 13 constexpr int sqr (int x) { return x * x; } int foo () { return sqr (2 ); } int main () { int i = foo (); return 0 ; }
以上代码在gcc中-O0时,仍然会生成sqr的函数调用过程(见godbolt )。
consteval
则是20标准中加入的强化版本的constexpr
,consteval
修饰的函数必须是编译期执行,并且consteval
只能用于修饰函数,关键字的含义为const evaluation
。
以上代码改用consteval
后:
1 2 3 consteval int sqr (int x) { return x * x; }
编译 生成汇编中不再有sqr函数调用。
由于是语法层面的强制编译期函数,如果无法执行编译期运算则编译出错:
1 2 3 4 5 6 7 8 9 10 11 12 13 int sqr (int x) { return x * x; } consteval int foo (int x) { return sqr (x); } int main () { int i = foo (2 ); return 0 ; }
编译结果见godbolt 。
constinit
constinit
关键字仅可用于变量,表明该变量拥有静态初始化,即该变量的初值为编译期常量,亦即const initialize
,仅可用于静态存储器和线程存储期的变量。听起来和constexpr
变量有点像,同样是常量初始化,但其实有区别:
constexpr
修饰的变量隐含该变量是const
,constinit
变量仅表示该变量初值为常量。如:
1 2 3 4 5 6 7 8 9 10 constinit int i = 1 ; int main () { constexpr int j = 1 ; i = 2 ; j = 2 ; return 0 ; }
编译结果godbolt 。
c++20开始constexpr
变量必须拥有常量析构,但是constinit
变量不需要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <memory> #include <vector> struct A { constexpr A (int i) : i_(i) { } constexpr ~A () = default ; int i_; }; constexpr A a{1 };constexpr std::shared_ptr<int > p = nullptr ;constinit std::shared_ptr<int > ap = nullptr ;
编译结果godbolt 。
–TO BE CONTINUED