0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C++23特性概览

CPP开发者 来源:CPP开发者 2023-02-02 10:28 次阅读

新年伊始,要说什么选题最合适,那无疑是C++23了。

23是个小版本,主要在于「完善」二字,而非「新增」。因此,值得单独拿出来写篇文章的特性其实并不多,大多特性都是些琐碎小点,三言两语便可讲清。

本篇包含绝大多数C++23特性,难度三星就表示只会介绍基本用法,但有些特性的原理也会深入讲讲。

1Deducing this(P0847)

Deducing this是C++23中最主要的特性之一。msvc在去年3月份就已支持该特性,可以在v19.32之后的版本使用。

为什么我们需要这个特性?

大家知道,成员函数都有一个隐式对象参数,对于非静态成员函数,这个隐式对象参数就是this指针;而对于静态成员函数,这个隐式对象参数被定义为可以匹配任何参数,这仅仅是为了保证重载决议可以正常运行。

Deducing this所做的事就是提供一种将非静态成员函数的「隐式对象参数」变为「显式对象参数」的方式。为何只针对非静态成员函数呢?因为静态成员函数并没有this指针,隐式对象参数并不能和this指针划等号,静态函数拥有隐式对象参数只是保证重载决议能够正常运行而已,这个参数没有其他用处。

于是,现在便有两种写法编写非静态成员函数。

1structS_implicit{
2voidfoo(){}
3};
4
5structS_explicit{
6voidfoo(thisS_explicit&){}
7};

通过Deducing this,可以将隐式对象参数显式地写出来,语法为this+type。

该提案最根本的动机是消除成员函数修饰所带来的冗余,举个例子:

 1//Before
 2structS_implicit{
 3intdata_;
 4
 5int&foo()&{returndata_;}
 6constint&foo()const&{returndata_;}
 7};
 8
 9//After
10structS_explicit{
11intdata_;
12
13template
14auto&&foo(thisSelf&self){
15returnstd::forward(self).data_;
16}
17};
原本你也许得为同一个成员函数编写各种版本的修饰,比如&, const&, &&, const &&,其逻辑并无太大变化,完全是重复的机械式操作。如今借助Deducing this,你只需编写一个版本即可。 这里使用了模板形式的参数,通常来说,建议是使用Self作为显式对象参数的名称,顾名思义的同时又能和其他语言保持一致性。 该特性还有许多使用场景,同时也是一种新的定制点表示方式。 比如,借助Deducing this,可以实现递归Lambdas
1intmain(){
2autogcd=[](thisautoself,inta,intb)->int{
3returnb==0?a:self(b,a%b);
4};
5
6std::cout<< gcd(20, 30) << "
";
7}
这使得Lambda函数再次得到增强。 又比如,借助Deducing this,可以简化CRTP
 1////Before
 2//CRTP
 3template
 4structBase{
 5voidfoo(){
 6auto&self=*static_cast(this);
 7self.bar();
 8}
 9};
10
11structDerived:Base{
12voidbar()const{
13std::cout<< "CRTP Derived
";
14    }
15};
16
17////////////////////////////////////////////
18//// After
19// Deducing this
20struct Base {
21    template 
22voidfoo(thisSelf&self){
23self.bar();
24}
25};
26
27structDerived:Base{
28voidbar()const{
29std::cout<< "Deducing this Derived
";
30    }
31};
这种新的方式实现CRTP,可以省去CR,甚至是T,要更加自然,更加清晰。 这也是一种新的定制点方式,稍微举个简单点的例子:
 1//Library
 2namespacemylib{
 3
 4structS{
 5autoabstract_interface(thisauto&self,intparam){
 6self.concrete_algo1(self.concrete_algo2(param));
 7}
 8};
 9}//namespacemylib
10
11namespaceuserspace{
12structM:mylib::S{
13autoconcrete_algo1(intval){}
14autoconcrete_algo2(intval)const{
15returnval*6;
16}
17};
18}//namespaceuserspace
19
20intmain(){
21usinguserspace::M;
22Mm;
23m.abstract_interface(4);
24}
这种方式依旧属于静态多态的方式,但代码更加清晰、无侵入,并支持显式opt-in,是一种值得使用的方式。 定制点并非一个简单的概念,若是看不懂以上例子,跳过便是。 下面再来看其他的使用场景。 Deducing this还可以用来解决根据closure类型完美转发Lambda捕获参数的问题。 亦即,如果Lambda函数的类型为左值,那么捕获的参数就以左值转发;如果为右值,那么就以右值转发。下面是一个例子:
 1#include
 2#include
 3#include//forstd::forward_like
 4
 5autoget_message(){
 6return42;
 7}
 8
 9structScheduler{
10autosubmit(auto&&m){
11std::cout<< std::boolalpha;
12        std::cout << std::is_lvalue_reference::value<< "
";
13        std::cout << std::is_rvalue_reference::value<< "
";
14        return m;
15    }
16};
17
18int main() {
19    Scheduler scheduler;
20    auto callback = [m=get_message(), &scheduler](this auto&& self) ->bool{
21returnscheduler.submit(std::forward_like(m));
22};
23callback();//retry(callback)
24std::move(callback)();//try-or-fail(rvalue)
25}
26
27//Output:
28//true
29//false
30//false
31//true
若是没有Deducing this,那么将无法简单地完成这个操作。 另一个用处是可以将this以值形式传递,对于小对象来说,可以提高性能。 一个例子:
 1structS{
 2intdata_;
 3intfoo();//implicitthispointer
 4//intfoo(thisS);//Passthisbyvalue
 5};
 6
 7intmain(){
 8Ss{42};
 9returns.foo();
10}
11
12// implicit this pointer生成的汇编代码:
13//subrsp,40;00000028H
14//learcx,QWORDPTRs$[rsp]
15//movDWORDPTRs$[rsp],42;0000002aH
16//callintS::foo(void);S::foo
17//addrsp,40;00000028H
18//ret0
19
20// Pass this by value生成的汇编代码:
21//movecx,42;0000002aH
22//jmpstaticintS::foo(thisS);S::foo
对于隐式的this指针,生成的汇编代码需要先分配栈空间,保存this指针到rcx寄存器中,再将42赋值到data_中,然后调用foo(),最后平栈。 而以值形式传递this,则无需那些操作,因为值传递的this不会影响s变量,中间的步骤都可以被优化掉,也不再需要分配和平栈操作,所以可以直接将42保存到寄存器当中,再jmp到foo()处执行。 Deducing this是个单独就可写篇四五星难度文章的特性,用处很多,值得深入探索的地方也很多,所以即便是概述这部分也写得比较多。

2Monadicstd::optional(P0798R8)

P0798提议为std::optional增加三个新的成员:map(), and_then()和or_else()。 功能分别为:

map:对optional的值应用一个函数,返回optional中wrapped的结果。若是optional中没有值,返回一个空的optional;

and_then:组合使用返回optional的函数;

or_else:若是有值,返回optional;若是无值,则调用传入的函数,在此可以处理错误。

在R2中map()被重命名为transform(),因此实际新增的三个函数为transform(),and_then()和or_else()。 这些函数主要是避免手动检查optional值是否有效,比如:

1//Before
2if(opt_string){
3std::optionali=stoi(*opt_string);
4}
5
6//After
7std::optionali=opt_string.and_then(stoi);
一个使用的小例子:
1//chainaseriesoffunctionsuntilthere'sanerror
2std::optionalopt_string("10");
3std::optionali=opt_string
4.and_then(std::stoi)
5.transform([](autoi){returni*2;});
错误的情况:
1//fails,transformnotcalled,j==nullopt
2std::optionalopt_string_bad("abcd");
3std::optionalj=opt_string_bad
4.and_then(std::stoi)
5.transform([](autoi){returni*2;});
目前GCC 12,Clang 14,MSVC v19.32已经支持该特性。

3std::expected(P0323)

该特性用于解决错误处理的问题,增加了一个新的头文件。 错误处理的逻辑关系为条件关系,若正确,则执行A逻辑;若失败,则执行B逻辑,并需要知道确切的错误信息,才能对症下药。 当前的常用方式是通过错误码或异常,但使用起来还是多有不便。 std::expected表示期望,算是std::variant和std::optional的结合,它要么保留T(期望的类型),要么保留E(错误的类型),它的接口又和std::optional相似。 一个简单的例子:

 1enumclassStatus:uint8_t{
 2Ok,
 3connection_error,
 4no_authority,
 5format_error,
 6};
 7
 8boolconnected(){
 9returntrue;
10}
11
12boolhas_authority(){
13returnfalse;
14}
15
16boolformat(){
17returnfalse;
18}
19
20std::expectedread_data(){
21if(!connected())
22returnstd::unexpected{Status::connection_error};
23if(!has_authority())
24returnstd::unexpected{Status::no_authority};
25if(!format())
26returnstd::unexpected{Status::format_error};
27
28return{"myexpectedtype"};
29}
30
31
32intmain(){
33autoresult=read_data();
34if(result){
35std::cout<< result.value() << "
";
36    } else {
37        std::cout << "error code: " << (int)result.error() << "
"; 
38    }
39}

这种方式无疑会简化错误处理的操作。

该特性目前在GCC 12,Clang 16(还未发布),MSVC v19.33已经实现。

4MultidimensionalArrays(P2128)

这个特性用于访问多维数组,之前C++operator[]只支持访问单个下标,无法访问多维数组。

因此要访问多维数组,以前的方式是:

重载operator(),于是能够以m(1, 2)来访问第1行第2个元素。但这种方式容易和函数调用产生混淆;

重载operator[],并以std::initializer_list作为参数,然后便能以m[{1, 2}]来访问元素。但这种方式看着别扭。

链式链接operator[],然后就能够以m[1][2]来访问元素。同样,看着别扭至极。

定义一个at()成员,然后通过at(1, 2)访问元素。同样不方便。

感谢该提案,在C++23,我们终于可以通过m[1, 2]这种方式来访问多维数组。

一个例子:

 1template
 2structmatrix{
 3T&operator[](constsize_tr,constsize_tc)noexcept{
 4returndata_[r*C+c];
 5}
 6
 7constT&operator[](constsize_tr,constsize_tc)constnoexcept{
 8returndata_[r*C+c];
 9}
10
11private:
12std::arraydata_;
13};
14
15
16intmain(){
17matrixm;
18m[0,0]=0;
19m[0,1]=1;
20m[1,0]=2;
21m[1,1]=3;
22
23for(autoi=0;i< 2; ++i) {
24        for (auto j = 0; j < 2; ++j) {
25            std::cout << m[i, j] << ' ';
26        }
27        std::cout << std::endl;
28    }
29}

该特性目前在GCC 12和Clang 15以上版本已经支持。

5if consteval(P1938)

该特性是关于immediate function的,即consteval function。 解决的问题其实很简单,在C++20,consteval function可以调用constexpr function,而反过来却不行。

 1constevalautobar(intm){
 2returnm*6;
 3}
 4
 5constexprautofoo(intm){
 6returnbar(m);
 7}
 8
 9
10intmain(){
11[[maybe_unused]]autores=foo(42);
12}
以上代码无法编译通过,因为constexpr functiong不是强保证执行于编译期,在其中自然无法调用consteval function。 但是,即便加上if std::is_constant_evaluated()也无法编译成功。
1constexprautofoo(intm){
2if(std::is_constant_evaluated()){
3returnbar(m);
4}
5return42;
6}
这就存在问题了,P1938通过if consteval修复了这个问题。在C++23,可以这样写:
1constexprautofoo(intm){
2ifconsteval{
3returnbar(m);
4}
5return42;
6}
该特性目前在GCC 12和Clang 14以上版本已经实现。

6FormattedOutput(P2093)

该提案就是std::print(),之前已经说过,这里再简单地说下。

标准cout的设计非常糟糕,具体表现在:

可用性差,基本没有格式化能力;

会多次调用格式化I/0函数;

默认会同步标准C,性能低;

内容由参数交替组成,在多线程环境,内容会错乱显示;

二进制占用空间大;

……

随着Formatting Library加入C++20,已在fmt库中使用多年的fmt::print()加入标准也是顺理成章。

格式化输出的目标是要满足:可用性、Unicode编码支持、良好的性能,与较小的二进制占用空间。为了不影响现有代码,该特性专门加了一个新的头文件,包含两个主要函数:

1#include
2
3intmain(){
4constchar*world="world";
5std::print("Hello{}",world);//doesn'tprintanewline
6std::println("Hello{}",world);//printanewline
7}
这对cout来说绝对是暴击,std::print的易用性和性能简直完爆它。 其语法就是Formatting Library的格式化语法。 性能对比:
----------------------------------------------------------
BenchmarkTimeCPUIterations
----------------------------------------------------------
printf87.0ns86.9ns7834009
ostream255ns255ns2746434
print78.4ns78.3ns9095989
print_cout89.4ns89.4ns7702973
print_cout_sync91.5ns91.4ns7903889
结果显示,printf与print几乎要比cout快三倍,print默认会打印到stdout。当打印到cout并同步标准C的流时(print_cout_sync),print大概要快14%;当不同步标准C的流时(print_cout),依旧要快不少。 遗憾的是,该特性目前没有编译器支持。

7FormattingRanges(P2286)

同样属于Formatting大家族,该提案使得我们能够格式化输出Ranges。 也就是说,我们能够写出这样的代码:

importstd;

automain()->int{
std::vectorvec{1,2,3};
std::print("{}
",vec);//Output:[1,2,3]
}
这意味着再也不用迭代来输出Ranges了。 这是非常有必要的,考虑一个简单的需求:文本分割。 Python的实现:
1print("howyoudoing".split(""))
2
3#Output:
4#['how','you','doing']
Java的实现:
 1importjava.util.Arrays;
 2
 3classMain{
 4publicstaticvoidmain(Stringargs[]){
 5System.out.println("howyoudoing".split(""));
 6System.out.println(Arrays.toString("howyoudoing".split("")));
 7}
 8}
 9
10//Output:
11//[Ljava.lang.String;@2b2fa4f7
12//[how,you,doing]
Rust的实现:
 1useitertools::Itertools;
 2
 3fnmain(){
 4println!("{:?}","Howyoudoing".split(''));
 5println!("[{}]","Howyoudoing".split('').format(","));
 6println!("{:?}","Howyoudoing".split('').collect::>());
 7}
 8
 9//Output:
10//Split(SplitInternal{start:0,end:13,matcher:CharSearcher{haystack:"Howyoudoing",finger:0,finger_back:13,needle:'',utf8_size:1,utf8_encoded:[32,0,0,0]},allow_trailing_empty:true,finished:false})
11//[How,you,doing]
12//["How","you","doing"]
JS的实现:
1console.log('Howyoudoing'.split(''))
2
3//Output:
4//["How","you","doing"]
Go的实现:
 1packagemain
 2import"fmt"
 3import"strings"
 4
 5funcmain(){
 6fmt.Println(strings.Split("Howyoudoing",""));
 7}
 8
 9//Output:
10//[Howyoudoing]
Kotlin的实现:
1funmain(){
2println("Howyoudoing".split(""));
3}
4
5//Output:
6//[How,you,doing]
C++的实现:
 1intmain(){
 2std::string_viewcontents{"Howyoudoing"};
 3
 4autowords=contents
 5|std::split('')
 6|std::transform([](auto&&str){
 7returnstd::string_view(&*str.begin(),std::distance(str));
 8});
 9
10std::cout<< "[";
11    char const* delim = "";
12    for (auto word : words) {
13        std::cout << delim;
14
15        std::cout << std::quoted(word);
16        delim = ", ";
17    }
18    std::cout << "]
";
19}
20
21// Output:
22// ["How", "you", "doing"]
借助fmt,可以简化代码:
 1intmain(){
 2std::string_viewcontents{"Howyoudoing"};
 3
 4autowords=contents
 5|std::split('')
 6|std::transform([](auto&&str){
 7returnstd::string_view(&*str.begin(),std::distance(str));
 8});
 9
10fmt::print("{}
",words); 
11fmt::print("<<{}>>",fmt::join(words,"--"));
12
13}
14
15//Output:
16//["How","you","doing"]
17//<>

因为views::split()返回的是一个subrange,因此需要将其转变成string_view,否则,输出将为:

 1intmain(){
 2std::string_viewcontents{"Howyoudoing"};
 3
 4autowords=contents|std::split('');
 5
 6fmt::print("{}
",words);
 7fmt::print("<<{}>>",fmt::join(words,"--"));
 8
 9}
10
11//Output:
12//[[H,o,w],[y,o,u],[d,o,i,n,g]]
13//<<['H', 'o', 'w']--['y', 'o', 'u']--['d', 'o', 'i', 'n', 'g']>>

总之,这个特性将极大简化Ranges的输出,是值得兴奋的特性之一。

该特性目前没有编译器支持。

7import std(P2465)

C++20模块很难用的一个原因就是标准模块没有提供,因此这个特性的加入是自然趋势。

现在,可以写出这样的代码:

1importstd;
2
3intmain(){
4std::print("Hellostandardlibrarymodules!
");
5}

性能对比:

6a02ac70-a29e-11ed-bfe3-dac502259ad0.png

如何你是混合C和C++,那可以使用std.compat module,所有的C函数和标准库函数都会包含进来。

目前基本没有编译器支持此特性。

8out_ptr(P1132r8)

23新增了两个对于指针的抽象类型,std::out_ptr_t和std::inout_ptr_t,两个新的函数std::out_ptr()和std::inout_ptr()分别返回这两个类型。 主要是在和C API交互时使用的,一个例子对比一下:

 1//Before
 2intold_c_api(int**);
 3
 4intmain(){
 5autoup=std::make_unique(5);
 6
 7int*up_raw=up.release();
 8if(intec=foreign_resetter(&up)){
 9returnec;
10}
11
12up.reset(up_raw);
13}
14
15////////////////////////////////
16//After
17intold_c_api(int**);
18
19intmain(){
20autoup=std::make_unique(5);
21
22if(intec=foreign_resetter(std::inout_ptr(up))){
23returnec;
24}
25
26//*upisstillvalid
27}

该特性目前在MSVC v19.30支持。

9auto(x) decay copy(P0849)

该提案为auto又增加了两个新语法:auto(x)和auto{x}。两个作用一样,只是写法不同,都是为x创建一份拷贝。

为什么需要这么个东西?

看一个例子:

1voidbar(constauto&);
2
3voidfoo(constauto¶m){
4autocopy=param;
5bar(copy);
6}
foo()中调用bar(),希望传递一份param的拷贝,则我们需要单独多声明一个临时变量。或是这样:
1voidfoo(constauto¶m){
2bar(std::decay_t{param});
3}
这种方式需要手动去除多余的修饰,只留下T,要更加麻烦。 auto(x)就是内建的decay copy,现在可以直接这样写:
1voidfoo(constauto¶m){
2bar(auto{param});
3}
大家可能还没意识到其必要性,来看提案当中更加复杂一点的例子。
 1voidpop_front_alike(auto&container){
 2std::erase(container,container.front());
 3}
 4
 5intmain(){
 6std::vectorfruits{"apple","apple","cherry","grape",
 7"apple","papaya","plum","papaya","cherry","apple"};
 8pop_front_alike(fruits);
 9
10fmt::print("{}
",fruits);
11}
12
13//Output:
14//["cherry","grape","apple","papaya","plum","papaya","apple"]

请注意该程序的输出,是否如你所想的一样。若没有发现问题,请让我再提醒一下:pop_front_alike()要移除容器中所有跟第1个元素相同的元素。

因此,理想的结果应该为:

["cherry","grape","papaya","plum","papaya","cherry"]
是哪里出了问题呢?让我们来看看gcc std::erase()的实现:
 1template
 2_ForwardIterator
 3__remove_if(_ForwardIterator__first,_ForwardIterator__last,
 4_Predicate__pred)
 5{
 6__first=std::__find_if(__first,__last,__pred);
 7if(__first==__last)
 8return__first;
 9_ForwardIterator__result=__first;
10++__first;
11for(;__first!=__last;++__first)
12if(!__pred(__first)){
13*__result=_GLIBCXX_MOVE(*__first);
14++__result;
15}
16
17return__result;
18}
19
20template
21inlinetypenamevector<_Tp, _Alloc>::size_type
22erase(vector<_Tp, _Alloc>&__cont,const_Up&__value)
23{
24constauto__osz=__cont.size();
25__cont.erase(std::remove(__cont.begin(),__cont.end(),__value),
26__cont.end());
27return__osz-__cont.size();
28}

std::remove()最终调用的是remove_if(),因此关键就在这个算法里面。这个算法每次会比较当前元素和欲移除元素,若不相等,则用当前元素覆盖当前__result迭代器的值,然后__result向后移一位。重复这个操作,最后全部有效元素就都跑到__result迭代器的前面去了。

问题出在哪里呢?欲移除元素始终指向首个元素,而它会随着元素覆盖操作被改变,因为它的类型为const T&。

此时,必须重新copy一份值,才能得到正确的结果。

故将代码小作更改,就能得到正确的结果。

voidpop_front_alike(auto&container){
autocopy=container.front();
std::erase(container,copy);
}

然而这种方式是非常反直觉的,一般来说这两种写法的效果应该是等价的。

我们将copy定义为一个单独的函数,表达效果则要好一点。

autocopy(constauto&value){
returnvalue;
}

voidpop_front_alike(auto&container){
std::erase(container,copy(container.front()));
}

而auto{x}和auto(x),就相当于这个copy()函数,只不过它是内建到语言里面的而已。

10Narrowingcontextualconversions to bool

这个提案允许在static_assert和if constexpr中从整形转换为布尔类型。

以下表格就可以表示所有内容。

Before After
if constexpr(bool(flags & Flags::Exec)) if constexpr(flags & Flags::Exec)
if constexpr(flags & Flags::Exec != 0) if constexpr(flags & Flags::Exec)
static_assert(N % 4 != 0); static_assert(N % 4);
static_assert(bool(N)); static_assert(N);

对于严格的C++编译器来说,以前在这种情境下int无法向下转换为bool,需要手动强制转换,C++23这一情况得到了改善。

目前在GCC 9和Clang 13以上版本支持该特性。

11forward_like(P2445)

这个在Deducing this那节已经使用过了,是同一个作者。 使用情境让我们回顾一下这个例子:

1autocallback=[m=get_message(),&scheduler](thisauto&&self)->bool{
2returnscheduler.submit(std::forward_like(m));
3};
4
5callback();//retry(callback)
6std::move(callback)();//try-or-fail(rvalue)

std::forward_like加入到了中,就是根据模板参数的值类别来转发参数。

如果closure type为左值,那么m将转发为左值;如果为右值,将转发为右值。

听说Clang 16和MSVC v19.34支持该特性,但都尚未发布。

12#eifdef and #eifndef(P2334)

这两个预处理指令来自WG14(C的工作组),加入到了C23。C++为了兼容C,也将它们加入到了C++23。

也是一个完善工作。

#ifdef和#ifndef分别是#if defined()和#if !defined()的简写,而#elif defined()和#elif !defined()却并没有与之对应的简写指令。因此,C23使用#eifdef和#eifndef来补充这一遗漏。

总之,是两个非常简单的小特性。目前已在GCC 12和Clang 13得到支持。

13#warning(P2437)

#warning是主流编译器都会支持的一个特性,最终倒逼C23和C++23也加入了进来。 这个小特性可以用来产生警告信息,与#error不同,它并不会停止翻译。 用法很简单:

1#ifndefFOO
2#warning"FOOdefined,performancemightbelimited"
3#endif

目前MSVC不支持该特性,其他主流编译器都支持。

14constexpr std::unique_ptr(P2273R3)

std::unique_ptr也支持编译期计算了,一个小例子:

1constexprautofun(){
2autop=std::make_unique(4);
3return*p;
4}
5
6intmain(){
7constexprautoi=fun();
8static_assert(4==i);
9}

目前GCC 12和MSVC v19.33支持该特性。

15improving string and string_view(P1679R3, P2166R1, P1989R2, P1072R10, P2251R1)

string和string_view也获得了一些增强,这里简单地说下。

P1679为二者增加了一个contain()函数,小例子:

1std::stringstr("dummytext");
2if(str.contains("dummy")){
3//dosomething
4}
目前GCC 11,Clang 12,MSVC v19.30支持该特性。 P2166使得它们从nullptr构建不再产生UB,而是直接编译失败。
1std::strings{nullptr};//error!
2std::string_viewsv{nullptr};//error!
目前GCC 12,Clang 13,MSVC v19.30支持该特性。 P1989是针对std::string_view的,一个小例子搞定:
1intmain(){
2std::vectorv{'a','b','c'};
3
4//Before
5std::string_viewsv(v.begin(),v.end());
6
7//After
8std::string_viewsv23{v};
9}

以前无法直接从Ranges构建std::string_view,而现在支持这种方式。

该特性在GCC 11,Clang 14,MSVC v19.30已经支持。

P1072为string新增了一个成员函数:

1template< class Operation >
2constexprvoidresize_and_overwrite(size_typecount,Operationop);
可以通过提案中的一个示例来理解:
1intmain(){
2std::strings{"Food:"};
3
4s.resize_and_overwrite(10,[](char*buf,intn){
5returnstd::find(buf,buf+n,':')-buf;
6});
7
8std::cout<< std::quoted(s) << '
'; // "Food"
9}

主要是两个操作:改变大小和覆盖内容。第1个参数是新的大小,第2个参数是一个op,用于设置新的内容。

然后的逻辑是:

如果maxsize <= s.size(),删除最后的size()-maxsize个元素;

如果maxsize > s.size(),追加maxsize-size()个默认元素;

调用erase(begin() + op(data(), maxsize), end())。

这里再给出一个例子,可以使用上面的逻辑来走一遍,以更清晰地理解该函数。

 1constexprstd::string_viewfruits[]{"apple","banana","coconut","date","elderberry"};
 2std::strings1{"Food:"};
 3
 4s1.resize_and_overwrite(16,[sz=s1.size()](char*buf,std::size_tbuf_size){
 5constautoto_copy=std::min(buf_size-sz,fruits[1].size());//6
 6std::memcpy(buf+sz,fruits[1].data(),to_copy);//append"banana"tos1.
 7returnsz+to_copy;//6+6
 8});
 9
10std::cout<< s1; // Food: banana

注意一下,maxsize是最大的可能大小,而op返回才是实际大小,因此逻辑的最后才有一个erase()操作,用于删除多余的大小。

这个特性在GCC 12,Clang 14,MSVC v19.31已经实现。

接着来看P2251,它更新了std::span和std::string_view的约束,从C++23开始,它们必须满足TriviallyCopyable Concept。

主流编译器都支持该特性。

最后来看P0448,其引入了一个新的头文件

大家都知道,stringstream现在被广泛使用,可以将数据存储到string或vector当中,但这些容器当数据增长时会发生「挪窝」的行为,若是不想产生这个开销呢?

提供了一种选择,你可以指定固定大小的buffer,它不会重新分配内存,但要小心数据超出buffer大小,此时内存的所有权在程序员这边。

一个小例子:

 1#defineASSERT_EQUAL(a,b)assert(a==b)
 2#defineASSERT(a)assert(a)
 3
 4intmain(){
 5charinput[]="102030";
 6std::ispanstreamis{std::span{input}};
 7inti;
 8
 9is>>i;
10ASSERT_EQUAL(10,i);
11
12is>>i;
13ASSERT_EQUAL(20,i);
14
15is>>i;
16ASSERT_EQUAL(30,i);
17
18is>>i;
19ASSERT(!is);
20}

目前GCC 12和MSVC v19.31已支持该特性。

16staticoperator()(P1169R4)

因为函数对象,Lambdas使用得越来越多,经常作为标准库的定制点使用。这种函数对象只有一个operator (),如果允许声明为static,则可以提高性能。

至于原理,大家可以回顾一下Deducing this那节的Pass this by value提高性能的原理。明白静态函数和非静态函数在重载决议中的区别,大概就能明白这点。

顺便一提,由于mutidimensional operator[]如今已经可以达到和operator()一样的效果,它也可以作为一种新的函数语法,你完全可以这样调用foo[],只是不太直观。因此,P2589也提议了static operator[]

17std::unreachable(P0627R6)

当我们知道某个位置是不可能执行到,而编译器不知道时,使用std::unreachalbe可以告诉编译器,从而避免没必要的运行期检查。 一个简单的例子:

 1voidfoo(inta){
 2switch(a){
 3case1:
 4//dosomething
 5break;
 6case2:
 7//dosomething
 8break;
 9default:
10std::unreachable();
11}
12}
13
14boolis_valid(inta){
15returna==1||a==2;
16}
17
18intmain(){
19inta=0;
20while(!is_valid(a))
21std::cin>>a;
22foo(a);
23}
该特性位于,在GCC 12,Clang 15和MSVC v19.32已经支持。

18std::to_underlying(P1682R3)

同样位于,用于枚举到其潜在的类型,相当于以下代码的语法糖:

static_cast>(e);

一个简单的例子就能看懂:

 1voidprint_day(inta){
 2fmt::print("{}
",a);
 3}
 4
 5enumclassDay:std::uint8_t{
 6Monday=1,
 7Tuesday,
 8Wednesday,
 9Thursday,
10Friday,
11Saturday,
12Sunday
13};
14
15
16intmain(){
17//Before
18print_day(static_cast>(Day::Monday));
19
20//C++23
21print_day(std::Friday));
22}
的确很简单吧!

该特性目前在GCC 11,Clang 13,MSVC v19.30已经实现。

19std::byteswap(P1272R4)

位于,顾名思义,是关于位操作的。

同样,一个例子看懂:

 1template
 2voidprint_hex(Tv)
 3{
 4for(std::size_ti=0;i< sizeof(T); ++i, v >>=8)
 5{
 6fmt::print("{:02X}",static_cast(T(0xFF)&v));
 7}
 8std::cout<< '
';
 9    }
10
11int main()
12{
13    unsigned char a = 0xBA;
14    print_hex(a);                     // BA
15    print_hex(std::byteswap(a));      // BA
16    unsigned short b = 0xBAAD;
17    print_hex(b);                     // AD BA
18    print_hex(std::byteswap(b));      // BA AD
19    int c = 0xBAADF00D;
20    print_hex(c);                     // 0D F0 AD BA
21    print_hex(std::byteswap(c));      // BA AD F0 0D
22    long long d = 0xBAADF00DBAADC0FE;
23    print_hex(d);                     // FE C0 AD BA 0D F0 AD BA
24    print_hex(std::byteswap(d));      // BA AD F0 0D BA AD C0 FE
25}
可以看到,其作用是逆转整型的字节序。当需要在两个不同的系统传输数据,它们使用不同的字节序时(大端小端),这个工具就会很有用。

该特性目前在GCC 12,Clang 14和MSVC v19.31已经支持。

20std::stacktrace(P0881R7, P2301R1)

位于,可以让我们捕获调用栈的信息,从而知道哪个函数调用了当前函数,哪个调用引发了异常,以更好地定位错误。

一个小例子:

1voidfoo(){
2autotrace=std::current();
3std::cout<< std::to_string(trace) << '
';
4}
5
6int main() {
7    foo();
8}

输出如下。

0#foo()at/app/example.cpp:5
1#at/app/example.cpp:10
2#at:0
3#at:0
4#

注意,目前GCC 12.1和MSVC v19.34支持该特性,GCC编译时要加上-lstdc++_libbacktrace参数。

std::stacktrace是std::basic_stacktrace使用默认分配器时的别名,定义为:

usingstacktrace=std::basic_stacktrace>;

而P2301,则是为其添加了PMR版本的别名,定义为:

namespacepmr{
usingstacktrace=
std::basic_stacktrace>;
}

于是使用起来就会方便一些。

 1//Before
 2charbuffer[1024];
 3
 4std::monotonic_buffer_resourcepool{
 5std::data(buffer),std::size(buffer)};
 6
 7std::basic_stacktrace<
 8    std::polymorphic_allocator>
 9trace{&pool};
10
11//After
12charbuffer[1024];
13
14std::monotonic_buffer_resourcepool{
15std::data(buffer),std::size(buffer)};
16
17std::stacktracetrace{&pool};

这个特性到时再单独写篇文章,在此不细论。

21Attributes(P1774R8, P2173R1, P2156R1)

Attributes在C++23也有一些改变。

首先,P1774新增了一个Attribute [[assume]],其实在很多编译器早已存在相应的特性,例如__assume()(MSVC, ICC),__builtin_assume()(Clang)。GCC没有相关特性,所以它也是最早实现标准[[assume]]的,目前就GCC 13支持该特性(等四月发布,该版本对Rangs的支持也很完善)。

现在可以通过宏来玩:

1#ifdefined(__clang__)
2#defineASSUME(expr)__builtin_assume(expr)
3#elifdefined(__GNUC__)&&!defined(__ICC)
4#defineASSUME(expr)if(expr){}else{__builtin_unreachable();}
5#elifdefined(_MSC_VER)||defined(__ICC)
6#defineASSUME(expr)__assume(expr)
7#endif
论文当中的一个例子:
1voidlimiter(float*data,size_tsize){
2ASSUME(size>0);
3ASSUME(size%32==0);
4
5for(size_ti=0;i< size; ++i) {
6        ASSUME(std::isfinite(data[i]));
7        data[i] = std::clamp(data[i], -1.0f, 1.0f);
8    }
9}

第一个是假设size永不为0,总是正数;第二个告诉编译器size总是32的倍数;第三个表明数据不是NaN或无限小数。

这些假设不会被评估,也不会被检查,编译器假设其为真,依此优化代码。若是假设为假,可能会产生UB。

使用该特性与否编译产生的指令数对比结果如下图。 6a146b72-a29e-11ed-bfe3-dac502259ad0.png

其次,P2173使得可以在Lambda表达式上使用Attributes,一个例子:

 1//Anyattributesospecifieddoesnotappertaintothefunction
 2//calloperatororoperatortemplateitself,butitstype.
 3autolam=[][[nodiscard]]->int{return42;};
 4
 5intmain()
 6{
 7lam();
 8}
 9
10//Output:
11//:Infunction'intmain()':
12//8:warning:ignoringreturnvalueof'',declaredwithattribute'nodiscard'[-Wunused-result]
13//12|lam();
14//|~~~^~
15//12:note:declaredhere
16//8|autolam=[][[nodiscard]]->int{return42;};
17//|^

注意,Attributes属于closure type,而不属于operator ()。

因此,有些Attributes不能使用,比如[[noreturn]],它表明函数的控制流不会返回到调用方,而对于Lambda函数是会返回的。

除此之外,此处我还展示了C++的另一个Lambda特性。

在C++23之前,最简单的Lambda表达式为[](){},而到了C++23,则是[]{},可以省略无参时的括号,这得感谢P1102。

早在GCC 9就支持Attributes Lambda,Clang 13如今也支持。

最后来看P2156,它移除了重复Attributes的限制。

简单来说,两种重复Attributes的语法评判不一致。例子:

1//Notallow
2[[nodiscard,nodiscard]]autofoo(){
3return42;
4}
5
6//Allowed
7[[nodiscard]][[nodiscard]]autofoo(){
8return42;
9}

为了保证一致性,去除此限制,使得标准更简单。

什么时候会出现重复Attributes,看论文怎么说:

During this discussion, it was brought up that
the duplication across attribute-specifiers are to support cases where macros are used to conditionally add attributes to an
attribute-specifier-seq, however it is rare for macros to be used to generate attributes within the same attribute-list. Thus,
removing the limitation for that reason is unnecessary.

在基于宏生成的时候可能会出现重复Attributes,因此允许第二种方式;宏生成很少使用第一种形式,因此标准限制了这种情况。但这却并没有让标准变得更简单。因此,最终移除了该限制。

目前使用GCC 11,Clang 13以上两种形式的结果将保持一致。

22Lambdas(P1102R2, P2036R3, P2173R1)

Lambdas表达式在C++23也再次迎来了一些新特性。

像是支持Attributes,可以省略(),这在Attributes这一节已经介绍过,不再赘述。

另一个新特性是P2036提的,接下来主要说说这个。

这个特性改变了trailing return typesName Lookup规则,为什么?让我们来看一个例子。

1doublej=42.0;
2//...
3autocounter=[j = 0]()mutable->decltype(j){
4returnj++;
5};

counter最终的类型是什么?是int吗?还是double?其实是double。

无论捕获列表当中存在什么值,trailing return type的Name Lookup都不会查找到它。

这意味着单独这样写将会编译出错:

1autocounter=[j=0]()mutable->decltype(j){
2returnj++;
3};
4
5//Output:
6//44:error:useofundeclaredidentifier'j'
7//autocounter=[j=0]()mutable->decltype(j){
8//^

因为对于trailing return type来说,根本就看不见捕获列表中的j。

以下例子能够更清晰地展示这个错误:

1templateintbar(int&,T&&);//#1
2templatevoidbar(intconst&,T&&);//#2
3
4inti;
5autof=[=](auto&&x)->decltype(bar(i,x)){
6returnbar(i,x);
7}
8
9f(42);//error
在C++23,trailing return types的Name Lookup规则变为:在外部查找之前,先查找捕获列表,从而解决这个问题。 目前没有任何编译器支持该特性。

23Literal suffixes for (signed) size_t(P0330R8)

这个特性为std::size_t增加了后缀uz,为signed std::size_t加了后缀z。

有什么用呢?看个例子:

1#include
2
3intmain(){
4std::vectorv{0,1,2,3};
5for(autoi=0u,s=v.size();i< s; ++i) {
6      /* use both i and v[i] */
7    }
8}

这代码在32 bit平台编译能够通过,而放到64 bit平台编译,则会出现错误:

1(5):errorC3538:inadeclarator-list'auto'mustalwaysdeducetothesametype
2(5):note:couldbe'unsignedint'
3(5):note:or'unsigned__int64'

在32 bit平台上,i被推导为unsigned int,v.size()返回的类型为size_t。而size_t在32 bit上为unsigned int,而在64 bit上为unsigned long long。(in MSVC)

因此,同样的代码,从32 bit切换到64 bit时就会出现错误。

而通过新增的后缀,则可以保证这个代码在任何平台上都能有相同的结果。

1#include
2
3intmain(){
4std::vectorv{0,1,2,3};
5for(autoi=0uz,s=v.size();i< s; ++i) {
6      /* use both i and v[i] */
7    }
8}

如此一来就解决了这个问题。

目前GCC 11和Clang 13支持该特性。

24std::mdspan(P0009r18)

std::mdspan是std::span的多维版本,因此它是一个多维Views。 看一个例子,简单了解其用法。

 1intmain()
 2{
 3std::vectorv={1,2,3,4,5,6,7,8,9,10,11,12};
 4
 5//Viewdataascontiguousmemoryrepresenting2rowsof6intseach
 6automs2=std::mdspan(v.data(),2,6);
 7//Viewthesamedataasa3Darray2x3x2
 8automs3=std::mdspan(v.data(),2,3,2);
 9
10//writedatausing2Dview
11for(size_ti=0;i!=ms2.extent(0);i++)
12for(size_tj=0;j!=ms2.extent(1);j++)
13ms2[i,j]=i*1000+j;
14
15//readbackusing3Dview
16for(size_ti=0;i!=ms3.extent(0);i++)
17{
18fmt::print("slice@i={}
",i);
19for(size_tj=0;j!=ms3.extent(1);j++)
20{
21for(size_tk=0;k!=ms3.extent(2);k++)
22fmt::print("{}",ms3[i,j,k]);
23fmt::print("
");
24}
25}
26}
目前没有编译器支持该特性,使用的是https://raw.githubusercontent.com/kokkos/mdspan/single-header/mdspan.hpp实现的版本,所以在experimental下面。 ms2是将数据以二维形式访问,ms3则以三维访问,Views可以改变原有数据,因此最终遍历的结果为:
1slice@i=0
201
323
445
5slice@i=1
610001001
710021003
810041005
这个特性值得剖析下其设计,这里不再深究,后面单独出一篇文章。

25flat_map, flat_set(P0429R9, P1222R4)

C++23多了flat version的map和set:

flat_map

flat_set

flat_multimap

flat_multiset

过去的容器,有的使用二叉树,有的使用哈希表,而flat版本的使用的连续序列的容器,更像是容器的适配器。

无非就是时间或空间复杂度的均衡,目前没有具体测试,也没有编译器支持,暂不深究。

26总结

本篇已经够长了,C++23比较有用的特性基本都包含进来了。

其中的另一个重要更新Ranges并没有包含。读至此,大家应该已经感觉到C++23在于完善,而不在于增加。没有什么全新的东西,也没什么太大的特性,那些就得等到C++26了。

大家喜欢哪些C++23特性?

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 参数
    +关注

    关注

    11

    文章

    1460

    浏览量

    31698
  • 函数
    +关注

    关注

    3

    文章

    4151

    浏览量

    61568
  • 指针
    +关注

    关注

    1

    文章

    475

    浏览量

    70404
  • C++
    C++
    +关注

    关注

    21

    文章

    2072

    浏览量

    73071

原文标题:C++23 特性概览

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    LLVM clang 公开 -std=c++23

    C++2a。 C++23 标准引入了许多新的特性,例如基于 Boost stacktrace 的 stacktrace library、conditionally borrowed ranges、兼容
    发表于 05-27 11:29

    EIA 364 23C

    EIA STANDARD TP-23CLOW LEVEL CONTACT RESISTANCETEST PROCEDURE FOR ELECTRICALCONNECTORS AND SOCKETSEIA/ECA-364-23C(Revision of EIA-364-
    发表于 08-10 18:43 34次下载

    低功耗数字VLSI设计:概览

    低功耗数字VLSI设计:概览:
    发表于 07-25 16:44 0次下载
    低功耗数字VLSI设计:<b class='flag-5'>概览</b>

    MEDICI的语法概览

    MEDICI 的语法概览 语句简介
    发表于 08-27 18:01 0次下载

    Apple平板iPad 概览

    Apple平板iPad 概览 规格: 尺寸:242.8
    发表于 02-01 11:26 939次阅读

    C8051F单片机产品概览

    C8051F单片机产品概览,又需要的朋友下来看看
    发表于 05-06 11:47 0次下载

    F2产品技术培训_1.产品特性概览

    F2产品技术培训_1.产品特性概览
    发表于 03-15 15:10 6次下载

    C#23种设计模式【完整】

    C#23种设计模式
    发表于 08-21 17:38 67次下载

    机器学习概览

    机器学习概览
    发表于 09-07 11:11 4次下载
    机器学习<b class='flag-5'>概览</b>

    CAT-D38999-DTS23C CAT-D38999-DTS23C 标准圆形连接器

    电子发烧友网为你提供TE(ti)CAT-D38999-DTS23C相关产品参数、数据手册,更有CAT-D38999-DTS23C的引脚图、接线图、封装手册、中文资料、英文资料,CAT-D38999-DTS23C真值表,CAT-D
    发表于 07-30 17:00

    EF3器件概览

    电子发烧友网站提供《EF3器件概览.pdf》资料免费下载
    发表于 09-27 09:13 0次下载
    EF3器件<b class='flag-5'>概览</b>

    Eagle器件概览

    电子发烧友网站提供《Eagle器件概览.pdf》资料免费下载
    发表于 09-27 09:22 2次下载
    Eagle器件<b class='flag-5'>概览</b>

    SALEAGLE FPGA器件概览

    电子发烧友网站提供《SALEAGLE FPGA器件概览 .pdf》资料免费下载
    发表于 09-27 09:16 0次下载
    SALEAGLE FPGA器件<b class='flag-5'>概览</b>

    C++23C++26新标准的展望

    而相对于C++23,个人更期待C++26,因为它会引入很多重磅的东西:executors、network、static reflection,希望到26时真的能看到这些,到时候C++也确实就更完善了。
    的头像 发表于 10-19 09:49 7666次阅读

    模拟输出及架构概览

    模拟输出及架构概览
    发表于 11-04 09:52 3次下载
    模拟输出及架构<b class='flag-5'>概览</b>