版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領
文檔簡介
現(xiàn)代C++語言核心特性解析C++23標準補充\h目錄 \h現(xiàn)代C++語言核心特性解析-C++23標準補充支持預處理指令#elifdef和#elifndef允許重復屬性允許static_assert聲明在與求值無關的模板上下文assume屬性assume屬性語法無法推導假設的條件表達式的情況初始化語句允許別名聲明允許在lambda表達式上使用屬性引入auto(x)和auto{x}代替decay-copychar8_t兼容性和可移植性修復引入翻譯字符集constevalif語句分隔的轉義序列顯式對象參數(shù)對象的不同類型引發(fā)的成員函數(shù)重載問題顯式對象參數(shù)語法調(diào)用具有顯式對象參數(shù)的函數(shù)指針靜態(tài)函數(shù)還是非靜態(tài)函數(shù)具有顯式對象參數(shù)的lambda表達式傳值的顯式對象參數(shù)私有繼承問題優(yōu)化CRTP對SFINAE友好總結標識符語法使用UAX31允許復合語句末尾的標簽(與C語言兼容)signedsize_t和size_t的字面量后綴z和uz可選的lambda表達式中的括號強制的類成員聲明順序布局多維下標運算符具名通用字符轉義明確static_assert和ifconstexpr支持bool縮窄轉換允許非字面量變量和goto語句的常量表達式函數(shù)進一步放寬常量表達式函數(shù)的限制禁止混合字符串字面量的連接刪除不可編碼的寬字符和多字寬字符字面量可選的擴展浮點類型允許static_asserts參數(shù)與ifconstexpr條件語句縮窄轉換到bool類型靜態(tài)下標運算符函數(shù)支持UTF-8作為可移植源文件編碼明確==和!=操作符的生成規(guī)則修剪行拼接符后的空格支持#warning預處理指令更簡單的隱式移動靜態(tài)函數(shù)調(diào)用運算符函數(shù)1.支持預處理指令#elifdef和 \h#elifndef一直以來,C++為了保持與C語言的兼容性會快速跟進C語言新增特性。這次也不例外,C23標準通過了加入#elifdef和#elifndef預處理指令的提案,于是C++23標準也加入了#elifdef和#elifndef這兩個預處理指令。我們知道,在C++23標準之前就已經(jīng)支持了許多條件預處理指令了,其中就包括:#if#ifexpr#ifdefidentifier#ifndefidentifier#elifexpr#else#endif比較有趣的是已經(jīng)可以很好的完成條件預編譯的任務了,但是標準還引入了已經(jīng)可以很好的完成條件預編譯的任務了,但是標準還引入了#ifdefidentifier和#ifndef#ifexpr#elifexpr#else#endif#ifdefined(identifier)#if!defined(identifier)identifier#ifdefined(identifier)#if!defined(identifier)當然了,雖然說有代替的寫法,但是對于我來說還是傾向少敲一些字符的。于是,跟我有相同想法的人就會提出疑惑,既然有#ifdefidentifier和#ifndefidentifier,為什么不提供對應的#elif呢?并且這部分的實現(xiàn)并不困難。現(xiàn)在,C23和C++23標準滿足了上述需求,提供了#elifdefidentifier#elifdefidentifier#elifndefidentifier兩個預處理指令,我們可以將其看作#elifdefined(identifier)#elifdefined(identifier)#elif!defined(identifier)的簡化版本。注意,兩種預處理指令的混用并不會造成不良后果,比如:#ifFOO#ifFOO#elifdefBAR#else#endif是不會有任何問題的,就如同我們現(xiàn)在寫以下代碼:#ifdefBAR#ifdefBAR#elifFOO#else#endif后來看C23標準中一段典型的用法:#ifdef__STDC__#ifdef__STDC__#defineTITLE“ISOCCompilation”#elifndef__cplusplus#defineTITLE“Non-ISOCCompilation”#else/*C++*/#defineTITLE“C++Compilation”#endif2.允許重復屬性 \hC++23標準允許屬性列表序列中存在重復屬性。(注意:該特性為C++11缺陷報告,所以編譯器實現(xiàn)可以將其加入到C++11標準。)例如:[[[[noreturn,noreturn]]voidfoo(){throw;}intmain(){foo();}使用GCC10和Clang12編譯上面的代碼會提示錯誤信息:error:attribute'noreturn'canappearatmostonceinanattribute-list因為在屬性列表中存在重復屬性noreturn,所以較老版本的編譯器會報錯。由于該缺陷報告是2020年7月提出的,因此使用較新版本的編譯器可以成功編譯,即使使用C++11標準。允許屬性列表存在重復屬性的理由很簡單,因為在缺陷修復之前,雖然屬性列表中不能聲明重復屬性,但是重復屬性可以聲明在不同的屬性列表,例如:上面這段代碼在任何支持屬性的編譯器版本都可以編譯成功,我們會發(fā)現(xiàn)上面這段代碼在任何支持屬性的編譯器版本都可以編譯成功,我們會發(fā)現(xiàn)]][[noreturn]][[noreturn[[noreturn]][[noreturn]]voidfoo(){throw;}intmain(){foo();}和[[noreturn,noreturn]]具有完全相同的含義,但前者可以通過編譯而后者不行,這顯然是不合理的,該缺陷的修改有助于屬性列表的行為表現(xiàn)一致。3.允許static_assert聲明在與求值無關\h的模板上下文缺陷報告CWG2518對許static_assert聲明做了一些改進,在CWG2518提出之前,下面這段代碼一定會編譯失?。涸蚝芎唵?,原因很簡單,static_assert(false);靜態(tài)斷言失敗,導致編譯器拒絕編譯。顯然,這樣是不合理template<classT>voidf(Tt){static_assert(false);}的。不合理的原因是,這段代碼只是簡單的定義了函數(shù)模板f,并沒有對其進行實例化,在這種情況下對其進行診斷是沒有必要的。為了解決上述問題,CWG2518對static_assert聲明做了一些修改,標準規(guī)定:在static_assert聲明中,常量表達式在上下文中被轉換為bool類型,轉換后的表達式應為常量表達式。如果表達式轉換后的值為true,或者表達式是在模板定義的上下文中求值的,則聲明無效。否則,static_assert聲明將失敗,程序將無法編譯,并產(chǎn)生診斷信息。注意這段描述中提到:或者表達式是在模板定義的上下文中求值的,則聲明無效。也就是說,如果只在模板定義中聲明static_assert,那么聲明是無效的。因此,我們?nèi)绻褂肎CC13和Clang17編譯這段代碼是可以順利編譯通過的。讓我們把這條規(guī)則延申一下,下面這段代碼使用CWG2518改進后的標準也能夠成功編譯:首先,我們知道定義函數(shù)模板首先,我們知道定義函數(shù)模板f是不會觸發(fā)靜態(tài)斷言失敗的,然后在實例化過程中ifconstexpr的特點是只編譯符合條件的分支,這里f(0)推導出T的類型是int,sizeof(T)==sizeof(int)的計算結果為true,else分支不會編譯,也就是說static_assert(false,"mustbeint-sized");這句代碼不會被編譯和診斷,編譯器順利完成編譯。當然,如果我們將函數(shù)模板調(diào)用從f(0)修改為f(c),那么就會導致靜態(tài)斷言失敗,從而引發(fā)編譯報錯。constexprvoiduse(intx){};template<classT>voidf(Tt){ifconstexpr(sizeof(T)==sizeof(int{))use(t);}else{static_assert(false,"mustbeint-sized";)}}voidg(charc){f(0;)}4.assume屬性 \hClang和MSVC都支持一種內(nèi)部函數(shù),該函數(shù)提供了一種方法優(yōu)化程序代碼的生成,簡單來說程序員可以給定一個表達式并假設該表達式的運算結果為true,編譯器會根據(jù)這個假設對代碼進行優(yōu)化,以便編譯器生成更快、更小的代碼,這個功能在高性能、低延遲計算中是非常有用的。我們先看看Clang中如何使用這個內(nèi)置函數(shù)優(yōu)化代碼:其中其中__builtin_assume是內(nèi)置假設函數(shù),該函數(shù)假設x>0為true以幫助編譯器優(yōu)化代碼。讓我們來對比一下使用假設內(nèi)置函數(shù)和沒有使用該函數(shù)的匯編代碼(編譯使用O3優(yōu)化選項):intdivide_by_32(intx{)__builtin_assume(x>0;)returnx/32;};使用__builtin_assume(x>0);divide_by_32(int):moveax,edishreax,5ret對比可以看出第一份代碼因為我們假設可以看出第一份代碼因為我們假設x>0,所以編譯器沒有為x為負數(shù)的情況生成代碼,直接采用邏輯右移完成計算。相反,第二份代碼需要區(qū)分x的正負符號,若x是正值,cmovnseax,edi執(zhí)行數(shù)據(jù)移動,并且做算術右移,相當于直接對edi寄存器的值算術右移5位;若x是負值,cmovnseax,edi不執(zhí)行數(shù)據(jù)移動,并且做算術右移,相當于對rdi+31的值算術右移5位。;沒有使用__builtin_assume(x>0);divide_by_32(int):leaeax,[rdi+31]testedi,edicmovnseax,edisareax,5ret通過上述例子,相信讀者朋友已經(jīng)了解了內(nèi)置假設函數(shù)功能的實用之處。但需要注意的是,使用這個功能必須對假設條件有足夠清晰的判斷,例如上面的例子中,我們必須確定x是大于0的,否則造成的后果是編譯器優(yōu)化了負數(shù)邏輯,導致當x是負數(shù)的時候函數(shù)計算出錯??梢哉f假設功能是個專家級功能,需要謹慎使用,若使用方法正確,可以取得非常不錯的優(yōu)化效果。不過可惜的是,該功能一直沒有標準進行規(guī)范,都是編譯器自定義實現(xiàn)的,例如:MSVCMSVC使用__assume(expr);Clang使用__builtin_assume(expr);GCC沒有這個功能,但是可以用內(nèi)置函數(shù)__builtin_unreachable();簡單模擬:我們可以使用一個宏來滿足移植要求:if(expr){}else{__builtin_unreachable();}不過,這樣的解決方案并不完美,比如不過,這樣的解決方案并不完美,比如GCC方案的代碼中,expr會在運行時進行求值計算的,而Clang和MSVC則不會,但是Clang和MSVC的內(nèi)置函數(shù)對于expr的要求也略有區(qū)別,所以很顯然我們需要一種標準化的方案來完成內(nèi)置假設函數(shù)的工作。#ifdefined(__clang__)#defineASSUME(expr)__builtin_assume(expr)#elifdefined(__GNUC__)#defineASSUME(expr)if(expr){}else{__builtin_unreachable();}#elifdefined(_MSC_VER)#defineASSUME(expr)__assume(expr)#endifassume屬性語法 \hC++23標準提出了一個統(tǒng)一的方案解決上述的問題,即添加一個新的assume屬性。使用assume作為屬性名的理由非常簡單直接,因為內(nèi)置函數(shù)就是使用的assume,所以它對于主流編譯器的用戶來說不會陌生,很容易了解它是用來干什么的。這里讀者朋友可能會提出疑問,原本編譯器使用的都是內(nèi)置函數(shù)執(zhí)行假設功能,為何標準選擇屬性來完成此功能呢?這是一個非常值得探討的問題,讓我們回想一下C++標準推出新功能的一般方法吧。首先,假設C++引入一個新的關鍵字,假定就是assume關鍵字吧,語法為assume(expr);。看起來不錯,但是這里的問題之一是引入新的關鍵字會影響老版本編譯器或者不支持新關鍵字編譯器的兼容性,另一個問題是引入新關鍵字對語言來說是一個非常重大的修改,而假設功能是一個專家級功能,使用它的時候必須非常小心謹慎,如果只是一小部分人使用的功能,那么新增一個關鍵字是不合適的。綜合看來,引入關鍵字是不合適的。其次,可以假定C++引入一個標準庫函數(shù),例如std::assume(expr),語法上看是庫函數(shù)調(diào)用,實際上expr并不求值。這樣做大的問題是,我們引入了一個看似庫函數(shù),但是行為模式和庫函數(shù)完全不同的“新類型”函數(shù)。它的行為和內(nèi)置函數(shù)一樣,你不能獲取它的函數(shù)地址,不能分配函數(shù)指針,與其說它是一個函數(shù),不如說是一個帶有命名空間的關鍵字。顯然,這種假定也不太合適。后,宏方案就更不必說了,C++一直致力于減少宏的使用,沒有任何理由讓C++引入一個新的宏。所以總體來說,assume作為一個新的屬性引入進來是合適的。因為,首先,屬性并不影響老版本編譯器和不支持該屬性的編譯器的兼容性,因為標準規(guī)定對于實現(xiàn)不支持的屬性可以直接忽略該屬性。這條規(guī)定正好又符合假設的另外一個特點,即忽略假設和忽略屬性一樣,都不會影響程序的正常邏輯。其次,屬性一般來說影響的都是代碼的生成和優(yōu)化,例如likely、unlikely、noreturn等,這些是針對編譯器的后端而不是前端,假設功能正好也是屬于代碼優(yōu)化功能??偨Y來說,assume作為屬性對語言的影響小,并且符合假設功能的特點,所以assume應該是一個屬性。好了,解釋了assume被定義為屬性的原因,現(xiàn)在讓我們來看看它的具體語法:[[assume(expr)]];這里assume屬性只能用于空語句,簡單來說就是屬性聲明之后直接跟隨;。expr必須是一個條件表達式,該表達式可以根據(jù)上下文轉換為bool,并且總是假設表達式的計算結果為true。如果表達式的計算結果確實為true,那么assume屬性不會對代碼邏輯產(chǎn)生任何影響(性能優(yōu)化,邏輯不變),否則會產(chǎn)生未定義的行為。產(chǎn)生未定義行為的邏輯很好理解,我們假設了一個錯誤的條件,編譯器根據(jù)錯誤條件的優(yōu)化是無法預測的。值得注意的是,因為expr是一個條件表達式,所以賦值表達式,逗號表達式都是錯誤語法:voidvoidf(){}boolg(boolx){[[assume(x=true)]];//編譯失敗[[assume(f(),x)]];//編譯失敗f();returnx;}當然,要讓以上代碼編譯成功也很簡單,只需要使用一對括號包括表達式即可:需要注意的是,這里必須強調(diào)一下[[assume((f(),x))]];這里必須強調(diào)一下[[assume((f(),x))]];的邏輯,因為它很容易讓人理解為,假設f()和x都必須返回true。實際上并不是這樣的,這里的假設是運行函數(shù)f()之后的副作用導致x返回值為true。voidf(){}boolg(boolx){[[assume((x=true;))]]//編譯成功[[assume((f(),x))]];//編譯成功f;()returnx;//直接編譯為returntrue;}intintdivide_by_32(intx){[[assume(y>0)]];//編譯失敗,不存在y的定義returnx/32;}上面的代碼會導致編譯報錯,GCC會明確提示:'y'wasnotdeclaredinthisscope'y'wasnotdeclaredinthisscope規(guī)則很簡單,但是還沒有結束,我還想給大家展示一個細節(jié),請看下面的代碼:constexprconstexprautof(inti){returnsizeof([=]{});}static_assert(f(0)==1);//lambda表達式類型的結構大小為1對比使用假設屬性后:constexprconstexprautof(inti){returnsizeof([=]{[[assume(i==0)]];});}static_assert(f(0)==4);//lambda表達式類型的結構大小為4神奇的情況發(fā)生了,我們剛才強調(diào)過,assume屬性并不會對表達式求值,但是lambda表達式的大小卻被改變了。這是為什么呢?其實,上文中已經(jīng)提到過了:“引用的變量和函數(shù)等必須在有其聲明的上下文中”。請注意,這兩份代碼中,前者因為的lambda表達式在函數(shù)體沒有捕獲任何變量,所以相當于[]{}這個lambda表達式,于是lambda表達式的字節(jié)大小為1。而后者則不同,為了能夠編譯成功,lambda表達式必須捕獲變量i,相當于[i]{},所以它的字節(jié)大小為4。這個問題影響大么?事實上并不大,首先作為屬性影響數(shù)據(jù)結構內(nèi)存布局和大小的assume并不是第一個,no_unique_address也是其中之一,可能區(qū)別是no_unique_address的作用就是影響數(shù)據(jù)結構的內(nèi)存布局和大小。其次,assume沒有影響到代碼的語義,程序在語義層面沒有變化。當然,也不是完全沒有影響,至少如果lambda表達式作為ABI,這個改變是會被觀察到的。后,需要說明一下關于常量表達式函數(shù)中使用假設的情況,如果在常量表達式函數(shù)內(nèi)部,我們遇到了一個假設,但在假設的條件表達式無法進行常量求值,例如:intintfoo(){return0;}constexprintbar(){[[assume(foo()==0)]];return1;}intmain(){constexprintretval=bar();returnretval;}結論是,上述代碼應該能夠順利的編譯通過。進行常量求值時,若無法在編譯時檢查假設,則應忽略該假設,而不是讓編譯器報錯。無法推導假設的條件表達式的情況 \h我們剛剛舉到的例子都是比較簡單的條件表達式,例如:這里直接給出了參數(shù)這里直接給出了參數(shù)x是大于0的假設,優(yōu)化沒有任何問題。如果將表達式復雜化一點呢?intdivide_by_32(intx){[[assume(x>0)]];returnx/32;}intdivide_by_32(intx){[[assume(-x<0)]];returnx/32;}使用GCC13編譯這份代碼也不會有問題,可以正確的優(yōu)化。因為雖然條件表達式不能求值,但是標準允許分析表達式的形式并推斷出用于優(yōu)化程序的信息。對于上述這種確定性的表達式,編譯器可以根據(jù)實際情況推導優(yōu)化信息。當然,現(xiàn)實世界的編譯器無法在所有情況下執(zhí)行所需的轉換,這并沒有關系,因為標準規(guī)定如果編譯器無法獲取任何有用的優(yōu)化信息,那么可以忽略該假設。例如:上面的代碼中,很明顯上面的代碼中,很明顯x的值是無法在編譯期確定的,也就無法推斷出有用的信息,所以GCC在這里的#include<iostream>intdivide_by_32(intx){[[assume((std::cin>>x,x>0))]];returnx/32;}做法是忽略該假設,不對代碼做任何優(yōu)化。請注意,這里還存在一個爭議點,那就是既然x>0是假設的一部分,那么是否應該假設std::cin>>x這個輸入值就是大于0的呢?這個論點是有道理的,例如我明確知道外部輸入是由另外一個程序執(zhí)行,并且輸入的數(shù)字一定大于0,那么這個假設就是有意義的,編譯器應該對它進行優(yōu)化,將出現(xiàn)未定義行為的責任拋給程序員。事實上,標準并沒有對這種情況做明確規(guī)定,編譯器完全可以根據(jù)實際情況做出選擇,使用完全不同的策略,其中就包括忽略假設。5.初始化語句允許別名聲明 \h我們知道,在現(xiàn)代我們知道,在現(xiàn)代C++代碼中,一直都提倡使用別名聲明using來代替typedef,但是C++23標準之前有一處卻有例外:#include<vector>intmain(){std::vector<int>vec{0,1,2};for(typedefintT;Ti:vec){}}上面這份代碼使用C++20標準可以編譯成功,但是如果修改為:#include<vector>#include<vector>intmain(){std::vector<int>vec{0,1,2};for(usingT=int;Ti:vec){}}新版本的Clang和GCC雖然可以編譯成功但是會給出警告,而舊版本編譯器會直接報錯。使用C++23標準則沒有上述問題,C++23標準擴展了初始化語句以允許別名聲明,這其中包括if、switch和基于范圍的for循環(huán)語句中的聲明。6.允許在lambda表達式上使用屬性 \h我們常說,lambda表達式是一種匿名函數(shù)對象,它可以在被調(diào)用的位置或作為參數(shù)傳遞給函數(shù)的位置定義,比起函數(shù)對象要方便很多。但是一直以來,函數(shù)對象有一個功能是lambda表達式不具備的,那就是我們可以給函數(shù)對象的函數(shù)調(diào)用運算符函數(shù)附加屬性,例如:在上面的代碼中,我們可以自定義的附加在上面的代碼中,我們可以自定義的附加Functor的函數(shù)調(diào)用運算符函數(shù)的屬性,例如這里我們附加了nodiscard屬性,這樣一來如果我們在調(diào)用f()的時候忽略其返回值,那么編譯器會給出警告提示我們需要獲取函數(shù)對象的返回值。structFunctor{[[nodiscard]]intoperator()(){return42;};}intmain(){Functorf;autoretval=f();//不能忽略返回值}現(xiàn)在問題來了,如果是編寫lambda表達式函數(shù),我們怎么能夠給它附加屬性呢?能夠這樣寫么?intintmain(){autolm=[[nodiscard]][]{return42;};//編譯報錯autoretval=lm();}顯然上面的代碼是無法編譯通過的,所以結論是在C++23標準以前我們無法給lambda表達式的函數(shù)調(diào)用運算符函數(shù)附加屬性。幸運的是,上述問題在C++23標準中得到了解決,在C++23標準中可以通過新的語法來給lambda表達式的函數(shù)調(diào)用運算符和運算符模板附加屬性了,其語法也非常簡單,在lambda表達式引導符即捕獲列表之后可以緊跟一個屬性聲明,例如:在上面的代碼中,空捕獲列表在上面的代碼中,空捕獲列表[]之后緊跟著一個nodiscard[[]],這樣lambda表達式的函數(shù)調(diào)用運算符函數(shù)就被聲明了nodiscard屬性,如果調(diào)用lm()的時候沒有接收返回值,那么編譯器會給出對應的intmain(){autolm=[][[nodiscard]]{return42;};autoretval=lm();//不能忽略返回值}警告信息。值得一提的是C++標準委員會在添加這個特性的時候,對于屬性聲明應該添加在lambda表達式的什么位置也是經(jīng)過非常深入的思考的。例如,像Functor的函數(shù)調(diào)用運算符函數(shù)那樣將屬性聲明在lambda表達式的開頭顯然不可取,這樣會造成語法的解析問題。那么合適的位置既然不能是開頭,但是又想和普通函數(shù)保持一致,那么只能放在函數(shù)名和參數(shù)列表之間,例如:structstructFunctor{intoperator()[[nodiscard]](){return42;}};intmain(){autoretval=Functor()();}在上面的代碼中,F(xiàn)unctor的函數(shù)調(diào)用運算符函數(shù)的屬性nodiscard也是生效的。所以對于lambda表達式而言,將屬性聲明放在捕獲列表和參數(shù)列表之間也是合適的地方了。autolm=[][[noreturn]](){throw;};//屬性聲明在捕獲列表[]和參數(shù)列表()之間7.引入auto(x)和auto{x}代替decay-copy \hC++23標準為auto占位符引入了一個新的功能,即使用auto(x)和auto{x}將x轉換為純右值。該特性重要的任務之一就是實現(xiàn)decay-copy功能,所謂decay-copy就是將一個值轉換為其退化的純右值副本,可以簡單實現(xiàn)為:templatetemplate<typenameT>constexprstd::decay_t<T>decay_copy(T&&v)noexcept(std::is_nothrow_convertible_v<T,std::decay_t<T>>){returnstd::forward<T>(v);}關于decay-copy的意義,我們先來看一個例子:classclassthread{public:template<classFunction,classArg>thread(Function&&f,Arg&&arg){//...std::invoke(std::forward<Function>(f),std::forward<Arg>(arg));//...}};上面這段代碼表達的是創(chuàng)建一個線程并且執(zhí)行目標函數(shù)的過程,其中用到了完美轉發(fā)保證參數(shù)屬性的一致性。但是這里有一個參數(shù)生命周期的問題,由于傳遞參數(shù)使用了完美轉發(fā),函數(shù)內(nèi)無法控制外部引用的生命周期,也就是說在線程執(zhí)行的過程中這些參數(shù)可能會無效導致未定義行為。要解決這個問題,就可以用到decay-copy:classclassthread{public:template<classFunction,classArg>thread(Function&&f,Arg&&arg){//...std::invoke(decay_copy(std::forward<Function>(f)),decay_copy(std::forward<Arg>(arg)));//...}};通過decay-copy的方法,創(chuàng)建了參數(shù)的純右值副本,這樣生命周期就在新線程的執(zhí)行函數(shù)中進行控制,不會出現(xiàn)變量失效導致未定義行為的問題。實際上,STL中的thread類也使用了這種技巧,但沒有明確定義decay-copy的函數(shù):另一個需要在語言層面代替decay_copy函數(shù)的原因在于,使用函數(shù)總是會有其局限性,例如提案文檔中的例子:在上面的代碼中,因為類在上面的代碼中,因為類A的構造函數(shù)被聲明為protected,那么decay_copy也沒辦法在這種情況下使用,畢竟我們不能讓decay_copy成為每個類的friend,但是如果使用語言層面的auto就不會存在這種限制。classA{intx;public:A();autorun(){f(A(*this;))//編譯成功f(auto(*this;))//編譯成功f(decay_copy(*this));//編譯失敗}protected:A(constA&;)};還有一個decay_copy從語義層面不能做到的是,調(diào)用此函數(shù)必然會進行拷貝操作,生產(chǎn)純右值副本,這里我們不考慮編譯器的優(yōu)化。但是auto則不同,它針對已經(jīng)是純右值的實體可以不做任何操作從而提高效率。關于auto(x)和auto{x}創(chuàng)建純右值副本的另一個有趣的問題是,decltype(auto(expr))和std::decay_t有什么區(qū)別?來看看下面兩份代碼:autop=std::make_unique<char>();static_assert(std::is_same_v<std::decay_t<decltype(p)>,std::unique_ptr<char>>);以及autop=std::make_unique<char>();static_assert(std::is_same_v<decltype(auto(p)),std::unique_ptr<char>>);它們在編譯時會有區(qū)別么?答案是前者只是簡單的做類型變換,比如將T&變換為T,并沒有被類型本身的定義約束,而后者則不同,auto會根據(jù)上下文環(huán)境對類型進行判斷,比如在上面的代碼中,因為std::unique_ptr<char>是沒有拷貝構造函數(shù)的,所以這里會編譯失敗,GCC提示信息為:error:useofdeletedfunction'std::unique_ptr<_Tp,_Dp>::unique_ptr(conststd::unique_ptr<_Tp,_Dp>&)[with_Tp=char;_Dp=std::default_delete<char>]'```#更改lambda表達式后置返回類型的解析范圍返回類型后置是lambda表達式的基本語法,這個語法在C++23標準之前存在一個問題,即返回類型解析范圍依賴于外部,比如下面這個例子:```c++autocounter=[j=0]()mutable->decltype(j){returnj++;};上面這段代碼中,counter是一個使用了初始化捕獲(init-capture)的lambda表達式,它的返回值類型為decltype(j),讀者可以思考一下,這份代碼是否可以編譯成功?答案是不能,因為外部并不存在j的聲明,而lambda表達式內(nèi)部的j對于decltype(j)來說是不可見的。在絕大多數(shù)情況下,這不會造成什么問題,畢竟編譯報錯會給程序員提示并修復自己的程序,但是也有例外情況:在聲明了外部的在聲明了外部的doublej=4.2;后,lambda表達式counter可以編譯成功了,但是卻產(chǎn)生了一個問題。這里decltype(j)的j使用了外部的變量聲明,也就是說counter的返回類型為double,而lambda表達式內(nèi)部基于初始化捕獲的j是一個int類型。我們可以使用以下代碼來驗證這個結論:doublej=4.2;autocounter=[j=0]()mutable->decltype(j){returnj++;};#include<type_traits>intmain(){doublej=4.2;static_assert(std::is_same_v<decltype(j),double>);autocounter=[j=0]()mutable->decltype(j){static_assert(std::is_same_v<decltype(j),int>);returnj++;};autoi=counter();static_assert(std::is_same_v<decltype(i),double>);}以上代碼使用MSVC19.35、Clang16和GCC13.1均可編譯成功。上述問題不僅存在于使用初始化捕獲的lambda表達式,其他lambda表達式同樣存在這樣的問題,只是觸發(fā)它并不容易:(下面內(nèi)容需要等待Clang17后續(xù)版本驗證,Clang17.0.1目前還有問題)上面這段代碼調(diào)用上面這段代碼調(diào)用f(42)會編譯失敗,因為返回類型推導中decltype(bar(i,x))中的i是lambda表達式外部的inti=0;的i,所以其類型為int,調(diào)用intbar(int&,T&&)函數(shù),返回int類型。而returnbar(i,x);語句中的i是lambda表達式捕獲的,由于lambda表達式?jīng)]有聲明為mutable,因此這里的i的類型為constinti,于是調(diào)用voidbar(intconst&,T&&)函數(shù),返回類型為void。lambda表達式f聲明的返回類型和實際返回類型不一致,導致編譯失敗。template<typenameT>intbar(int&,T&&);template<typenameT>voidbar(intconst&,T&&);intmain(){inti=0;autof=[=](auto&&x)->decltype(bar(i,x{))returnbar(i,x;)};f(42;)//編譯失敗}從C++23標準開始,上述問題得以解決,其解決方案簡單來說就是后置返回類型中的任何可捕獲實體的行為應該像被捕獲一樣,無論它終是否被捕獲。例如上面的代碼,在例如上面的代碼,在C++23標準之前,f的返回類型為int&。從C++23標準開始,f的返回類型為inti;autof=[=](int&j)->decltype((i)){returnj;;}constint&,即使lambda表達式實際上并沒有捕獲它。值得注意的是,這種特殊的待遇只適用于后置返回類型中的可捕獲實體,若可捕獲實體出現(xiàn)在函數(shù)參數(shù)范圍之前或位于參數(shù)聲明子句中,則會導致編譯錯誤。為了更明確新規(guī)則帶來的影響,我們來看看下面這份代碼:voidvoidf(){floatx=0.;[=]<decltype(x)P>{};//編譯錯誤:x可被捕獲,但在lambda的函數(shù)參數(shù)之前[=](decltype((x))y){};//編譯錯誤:x可被捕獲,但在lambda的函數(shù)參數(shù)聲明子句中[=]{[]<decltype(x)P>{};//編譯成功:x沒有被捕獲[](decltype((x))y){};//編譯成功:x沒有被捕獲[x=1](decltype((x))z){};//編譯錯誤:x是可被捕獲,但在lambda的函數(shù)參數(shù)參數(shù)聲明子句中};}代碼的注釋很明確的解釋了lambda表達式在C++23標準中編譯成功或者失敗的原因。總結來說,如果一個可捕獲的實體,比如例子中的局部變量x,聲明在lambda的函數(shù)參數(shù)之前或者聲明在參數(shù)聲明子句中,就會導致編譯失敗。但是如果該實體是不可捕獲的,例如捕獲列表為空[],那么代碼就可以編譯成功。8.char8_t兼容性和可移植性修復 \h根據(jù)上一節(jié)的內(nèi)容,我們了解了自C++20標準引入UTF-8字符類型以后,出現(xiàn)了與C++17標準不兼容的情況,例如:constchar*ptr0=u8"helloutf-8";//C++17:編譯成功|C++20:編譯失敗constunsignedchar*ptr1=u8"helloutf-8";//C++17:編譯失敗|C++20:編譯失敗constchararr0[]=u8"helloutf-8";//C++17:編譯成功|C++20:編譯失敗constunsignedchararr1[]=u8"helloutf-8";//C++17:編譯成功|C++20:編譯失敗constexprconstchar*resource_id(){returnu8"???";//C++17:編譯成功|C++20:編譯失敗}這個兼容問題的影響面比想象的要大,導致不少代碼庫在升級到C++20的時候無法成功編譯。為了解決這個問題,編譯器實現(xiàn)給出了編譯選項消除影響,例如,Clang和GCC可以通過-fno-char8_t或者MSVC通過/Zc:char8_t-告訴編譯器在使用C++20標準時,讓char8_t特性退回到C++17標準,以保證源代碼可以順利編譯。除了通過編譯器實現(xiàn)來解決上述問題,聰明的C++程序員也想到了模板元編程的辦法,例如:#include<utility>#include<utility>template<std::size_tN>structchar8_t_string_literal{staticconstexprinlinestd::size_tsize=N;template<std::size_t...I>constexprchar8_t_string_literal(constchar8_t(&r)[N],std::index_sequence<I...>):s{r[I]...}{}constexprchar8_t_string_literal(constchar8_t(&r)[N])已經(jīng)熟悉模板元編程和語言新特性的朋友理解上面這段代碼應該沒有問題,但是對于新手朋友可能會比較困難,我這里簡單解釋一下。char8_t_string_literal是一個字面量類模板,其模板參數(shù)N由構造函數(shù)constexprchar8_t_string_literal(constchar8_t(&r)[N])推導而來,這里使用的是類模板的模板實參推導特性。char8_t_string_literal(constchar8_t(&r)[N])沒有直接進行構造,而是調(diào)用template<std::size_t...I>constexprchar8_t_string_literal(constchar8_t(&r)[N],std::index_sequence<I...>)完成對象的構造,這里使用的是委托構造函數(shù)的特性。代理構造函數(shù)通過可變模板參數(shù)的包展開特性初始化了數(shù)據(jù)成員char8_ts[N];,完成對象的構造。as_char_buffer是通過變量模板的特性將char8_t數(shù)組轉換為char數(shù)組。make_as_char_buffer使用常量表達式函數(shù)和可變模板參數(shù)包展開的特性來構造as_char_buffer。operator""_as_char使用用戶自定義字面量的特性針對字符和字符串調(diào)用不同的函數(shù)。后通過__cpp_char8_t功能測試宏特性,在不同的編譯環(huán)境下預處理為不同的代碼。如果讀者朋友現(xiàn)在看不懂這份代碼也沒關系,上述提的特性在本書中都會詳細說明,讀完本書后再回頭理解這份代碼應該就不會有問題。這份代碼對兼容的實現(xiàn)方式雖然很“神奇”,但是正如我之前所說的,它不利于新手理解,屬于C++專家熱衷的代碼寫法。并且使用宏來區(qū)分編譯環(huán)境也并不是一個好主意,這很容易讓不熟悉編碼環(huán)境的程序員落入代碼陷阱。就像Windows編程中TCHAR和TEXT(...),程序員在編寫C++代碼時必須了解這些宏和編譯環(huán)境的關系以保證字符串被正確處理。順便提一句,Windows編程應該盡量使用Unicode版本的API,它們通常有W后綴,這樣就不會受到代碼頁改變的影響了。引入char8_t類型后,C++20標準除了和C++17標準產(chǎn)生了兼容性問題,與C語言的兼容性也受到了影響:externconstchar*a=u8"helloutf-8";//C:編譯成功|C++20:編譯失敗externconstcharb[]=u8"helloutf-8";//C:編譯成功|C++20:編譯失敗externconstunsignedchar*c=u8"helloutf-8";//C:編譯成功|C++20:編譯失敗externconstunsignedchard[]=u8"helloutf-8";//C:編譯成功|C++20:編譯失敗為了緩解這類兼容性問題,C++23標準對char8_t的轉換規(guī)則做出了一些修改(該修改也作為C++20的缺陷報告提出,因此新版本編譯器的C++20標準也支持該修改),標準規(guī)定:char或unsignedchar數(shù)組可以通過UTF-8字符串字面量或用大括號括起來的字符串字面量進行初始化。即下面的例子中,部分代碼可以重新編譯成功了:constchar*ptr0=u8"helloutf-8";//編譯失?。褐羔橆愋蜔o法轉換constunsignedchar*ptr1=u8"helloutf-8";//編譯失?。褐羔橆愋蜔o法轉換constchararr0[]=u8"helloutf-8";//編譯成功:數(shù)組轉換constunsignedchararr1[]{u8"helloutf-8"};//編譯成功:數(shù)組轉換注意,標準只是規(guī)定char或unsignedchar數(shù)組可以被UTF-8的字符串字面量初始化,而沒有提到對應的指針類型的轉換,所以指針類型的轉換依舊是非法的。不過,這個修改已經(jīng)足以解決大部分問題,使用指針的代碼只需要稍作修改就能兼容新老版本的編譯環(huán)境。至于指針之間的轉換,因為涉及到包括u8""如何退化為constchar*或constunsignedchar*,重載的排序是什么,以及在什么情況下適用等大量的工作,所以截至C++23標準為止,并未對其做出修改。后需要注意的是,因為允許后需要注意的是,因為允許UTF-8的字符串字面量隱式轉換為char或unsignedchar數(shù)組,所以對于包含數(shù)組的結構體初始化的重載解析會帶來一個問題:structA{char8_ts[10];};structB{chars[10];};voidf(A);voidf(B);intmain(){f({u8""});}上面這份代碼,可以使用Clang16和GCC12編譯成功,因為適配缺陷報告之前編譯器認為{u8""}只能用于初始化結構體A,所以重載決議很明確不會有問題。但是標準修改之后,結構體A和B都可以使用{u8""}進行聚合初始化,這就造成了調(diào)用函數(shù)f時的二義性問題。好在,這個問題的觸發(fā)條件比較罕見,我們也不必為此過于擔心了。9.引入翻譯字符集 \hC++23標準引入了翻譯字符集,簡單來說翻譯字符集就是翻譯時使用的抽象字符集,它是可以表示所有有效通用字符名稱的等效字符。翻譯字符集具體范圍包括:具有ISO/IEC10646命名,并且有唯一的UCS(UniversalCodedCharacterSet)標量值標識的每個字符,以及未分配命名的每個UCS標量值的獨立字符。翻譯字符集的作用在于,過去編譯器翻譯的第一階段,是由編譯器的實現(xiàn)來定義源文件的字符映射到基礎字符集的方式,這里的問題在于基礎字符集是一個非常小的集合,那么如何映射不在基礎字符集的字符就成了一個問題。從C++23標準開始,新的規(guī)定是源文件的字符應該映射到翻譯字符集,這個字符集范圍要大得多,基礎字符集是它的一個子集,這樣就解決了上述問題。舉例來說,在C++23之前,一個編譯器可以實現(xiàn)成這樣:因為因為?這個字符不是基礎字符集,所以可能發(fā)生轉義\u00f6?,F(xiàn)在根據(jù)C++23標準,?是可以被識別的#defineS(x)#xconstchar*s1=S(K?ppe;)//"K\\u00f6ppe"constchar*s2=S(K\u00f6ppe);//"K\\u00f6ppe"字符,那么結果就應該是:#defineS(x)#x#defineS(x)#xconstchar*s1=S(K?ppe);//"K?ppe"constchar*s2=S(K\u00f6ppe);//"K?ppe"然后,我們必須提到一個有趣的事實,現(xiàn)實情況是幾乎所有的主流編譯器實現(xiàn)都已經(jīng)實現(xiàn)了后者的功能,并沒有提供轉義UCN。#一致的字符字面量編碼在C++23標準之前,預處理器條件中的字符字面量的行與C++表達式中的行為是不一定一致的,例如:#if'A'=='\x41'#if'A'=='\x41'//...#endif與ifif('A'==0x41){}判斷的結果可能會有所不同。因為C++23之前的標準中明確表示,兩種代碼的行為是否一致是由實現(xiàn)定義的。也就是說編譯器可以決定上述代碼的行為結果。不過,對于我們程序員來說,當然是更希望它們有一致的行為結果,因為這種特性可以用于檢測字符編碼,例如sqlite頭文件sqliteInt.h中的這段代碼:/*/***ChecktoseeifthismachineusesEBCDIC.(Yes,believeitor**not,therearestillmachinesouttherethatuseEBCDIC.)*/#if'A'=='\301'#defineSQLITE_EBCDIC1#else#defineSQLITE_ASCII1#endif所以,C++23標準采納了這種實踐和用戶期望,規(guī)定預處理器條件中的字符字面量的行為應該與C++表達式中的行為一致。10.constevalif語句 \h前面的章節(jié)介紹了,C++20標準引入了consteval說明符和std::is_constant_evaluated()函數(shù)讓我們能夠方便的編寫常量求值的代碼。簡單回顧一下這兩個特性,consteval說明符用于只能在常量求值期間調(diào)用的函數(shù),也叫做立即函數(shù);而is_constant_evaluate()則是一個庫函數(shù),用于檢查當前求值計算是否為常量求值。不過遺憾的是,這兩個功能卻有些不太兼容,請注意以下代碼:這是提案文檔中了一份代碼,使用這是提案文檔中了一份代碼,使用C++20標準編譯這份代碼會出現(xiàn)編譯錯誤。原因很簡單,函數(shù)g中constevalintf(inti){returni;}constexprintg(inti){if(std::is_constant_evaluated()){returnf(i)+1;//<==}else{return42;}}constevalinth(inti{)returnf(i)+1;}f(i)+1不是一個常量表達式,其中i是不確定的。這里我們會發(fā)現(xiàn),std::is_constant_evaluated()完全沒起到想要的作用,代碼的本意是如果if的條件確定是常量求值,那么編譯器才應該編譯if中的這段代碼,而不是直接報錯。順帶一提,函數(shù)h是立即函數(shù),所以調(diào)用函數(shù)f不會有任何問題。很顯然,上面代碼的std::is_constant_evaluated()沒有到達我們預期的效果,為了解決這個問題,C++23標準引入了新的constevalif語句,它的語法比較獨特:ifconsteval{}else{}注意這里if后面的constevalifconsteval{}else{}含義是:如果代碼執(zhí)行的上下文在常量求值期間發(fā)生,則執(zhí)行if子語句,否則,如果存在選擇語句的else部分,則執(zhí)行else子語句。有了新的constevalif語句后,我們就可以修改上面的代碼為:constevalconstevalintf(inti){returni;}constexprintg(inti){ifconsteval{returnf(i)+1;//ok:immediatefunctioncontext}else{return42;}}constevalinth(inti){returnf(i)+1;//ok:immediatefunctioncontext}使用C++23標準可以順利的編譯這段代碼,不會出現(xiàn)之前的編譯錯誤。值得一提的是,我們可以通過constevalconstevalif語句來編寫一個自己的is_constant_evaluated函數(shù):constexprboolis_constant_evaluated(){ifconsteval{returntrue;}else{returnfalse;}}當然我并不推薦這么做,統(tǒng)一使用標準庫才是更好的選擇,提案文檔的作者也表示不應該棄用std::is_constant_evaluated()函數(shù),即使從現(xiàn)在看來正確的實現(xiàn)該函數(shù)并不困難,但是每個人都去實現(xiàn)一份相同的代碼顯然不是一個好的選擇,另外使用標準提供的std::is_constant_evaluated()函數(shù)能夠讓編譯器更準確的提供警告信息,例如:constexprconstexprintg(){ifconstexpr(std::is_constant_evaluated()){//warning:alwaystruereturn42;}return7;}編譯以上代碼會輸出如下警告信息:編譯以上代碼會輸出如下警告信息:再次強調(diào)一下,constevalif語句中的大括號是不能忽略的,以下代碼編譯器會提示缺少括號{:編譯以上代碼,編譯器會提示:warning:'std::is_constant_evaluated'willalwaysevaluateto'true'inamanifestlyconstant-evaluatedexpressionconstexprintg(inti){ifconsteval{returnf(i)+1;}elsereturn42;}error:expected{afterelseerror:expected{afterelse至于為何這樣強制規(guī)定,提案文檔上也給了解釋:Thereisnotechnicalreasontomandatebraces.Ourreasonforthemistoemphasizevisuallythatwe’redroppingintoanentirelydifferentcontext.很明確,并沒有技術上的理由,就是為了在視覺上強調(diào)代碼正在進入一個完全不同的環(huán)境而已,而這樣做的很重要的一部分原因是,ifconsteval中consteval沒有用括號包括起來,括號具有很明顯的分割視覺的作用。如果允許忽略大括號,那么就可能出現(xiàn)以下這種代碼:ifconstevalfor(;it!=end;++it);ifconstevalif(it!=end);顯然,這樣的代碼看起來會很頭痛。那么問題又來了,為何不將語法規(guī)定為if(consteval)呢?關于這一點,提案作者也給了簡單明了的回答:wethinkthefactthatifconstevallooksdifferentfromaregularifstatementisarguablyabenefit.總之,作者認為這種與眾不同的語法是有好處的,而且C++委員會也接受了這樣的提案。后來介紹一下constevalif語句的否定形式:ififnotconsteval{}//或者if!consteval{}注意,標準中強調(diào)ifif!constevalcompound-statement本身不是一個constevalif語句,但是它等價于constevalif語句:ififconsteval{}elsecompound-statement同理ifif!constevalcompound-statement1elsestatement2本身也不是一個constevalif語句,但是它等價于constevalif語句:ififconstevalstatement2elsecompound-statement111.分隔的轉義序列 \h轉義序列是C++語言的基本功能,我們常用的轉義序列包括簡單轉義序列\(zhòng)r、\n、\t等,數(shù)字轉義序列八進制的\nnn(1-3個八進制數(shù))、十六進制\xn...(任意個十六進制數(shù))以及通用字符轉義序列\(zhòng)unnnn(小寫u和4個十六進制數(shù))、\Unnnnnnnn(大寫U和8個十六進制數(shù))。其實對我來說,C++的轉義序列已經(jīng)足夠簡單實用了,但是C++委員會的專家還是提出了兩個問題。首先,通用字符轉移序列規(guī)定大寫字母U和小寫字母u后必須緊跟8個和4個十六進制數(shù),但是我們使用的Unicode代碼空間為0-0x10FFFF,大部分情況下我們用不到8個十六進制數(shù),例如\U0001F1F8,就需要填充3個無意義的0,是有改進空間的。#defineBell'\x07'其次,八進制和十六進制的數(shù)字轉義序列由于有可變的序列長度,所以使用起來容易出現(xiàn)一些不易察覺的錯誤。例如,\17會被解析為一個數(shù)值0x0f,但是如果寫成\18那么它會被解析為數(shù)值0x01和字符'8'。為了解決這個問題,微軟的提出了一些解決方案,比如使用宏,例如:#defineBell'\x07'通過宏來避免轉義序列和字符的混淆。另外,微軟還提出可以使用斷開字符串的技巧來區(qū)分轉義序列和字符,例如:"\xabc""\xabc"http://一個字符"\xab""c"http://兩個字符這兩個方法對解決上述問題確實有效,但是C++委員會認為這樣還不夠優(yōu)雅,所以提出了新的分隔方案。C++23標準規(guī)定:使用\o{}的語法來定義一個八進制轉義序列,其中{}中可以有任意個八進制數(shù);使用\x{}的語法來定義一個十六進制轉義序列,其中{}中可以有任意個十六進制數(shù);使用\u{}的語法來定義一個通用字符轉義序列,其中{}中可以有任意個十六進制數(shù),當然{}中的數(shù)值必須是一個有效的Unicode變量值。所以上述例子中"\xab""c"可以表示為"\x{ab}c"。注意,分隔轉義序列的處理發(fā)生在字符串連接之前,所以我們必須保證分隔轉義序列的完整性,例如:上面的代碼是會導致編譯報錯的,編譯器會提示上面的代碼是會導致編譯報錯的,編譯器會提示'\x{'不是由'}'結束。charstr[]="\x{4""2}";error:'\x{'notterminatedwith'}'after\x{4值得一提的是,分隔的轉義序列的這個語法和另一個新語言特性是密切相關的,那就是具名通用字符轉義。12.顯式對象參數(shù) \h自從C++11標準發(fā)布以來,C++的新標準一直致力于減少代碼中的冗余,例如auto占位符、基于范圍的for循環(huán)等等。但是由于右值引用的出現(xiàn),有一部分代碼卻比過去顯得更加冗余了,那就是成員函數(shù)的重載。對象的不同類型引發(fā)的成員函數(shù)重載問題 \h一般來說,成員函數(shù)可以有CV限定符,因此可能存在特定類同時需要特定成員函數(shù)的const和非const重載的情況。當然,也可能需要volatile重載,但這不太常見,為了描述簡潔我們可以忽略它。在有const重載的情況下,兩個函數(shù)基本上做的是相同的任務,唯一的區(qū)別是函數(shù)訪問的對象是否為常量類型。我們可以通過復制函數(shù)的實現(xiàn)代碼來處理,例如,STL中vector的運算符函數(shù)operator[][]:///microsoft/STL/blob/main/stl/inc/vector_Ty&operator[](constsize_type_Pos)noexcept{auto&_My_data=_Mypair._Myval2;return_My_data._Myfirst[_Pos];}const_Ty&operator[](constsize_type_Pos)constnoexcept{auto&_My_data=_Mypair._Myval2;return_My_data._Myfirst[_Pos];}或者將一個重載委托給另一個,比如可以將上述代碼改寫為:在上面這段代碼中,非在上面這段代碼中,非const版本的operator[]運算符函數(shù)委托調(diào)用了const版本的operator[]運_Ty&operator[](constsize_type_Pos)noexcept{returnconst_cast<_Ty&>(static_cast<constvector&>(*this)[_Pos]);}const_Ty&operator[](constsize_type_Pos)constnoexcept{auto&_My_data=_Mypair._Myval2;return_My_data._Myfirst[_Pos];}算符函數(shù)。雖然這份代碼沒有上一段直觀,但是更容易維護。不過話說回來,無論是復制代碼還是委托調(diào)用,似乎都不是很好的解決方案,尤其是C++11引入了右值引用以后。本來只需要維護2個重載函數(shù),現(xiàn)在變成了4個:&、const&、&&和const&&。我們有三種方案來編寫這4個函數(shù),包括上文中提到的兩種,分別是:實現(xiàn)4個重載函數(shù),這樣不可避免的會出現(xiàn)重復代碼;將其中3個重載函數(shù)委托給另外1個重載函數(shù),這種方法可以避免大量重復的代碼,但是過多的類型轉換讓代碼看起來并不優(yōu)雅;實現(xiàn)一個輔助函數(shù)模板,將4個重載函數(shù)委托到該輔助函數(shù)模板,讓編譯器來推導調(diào)用對象的真實類型,避免編寫類型轉換的代碼。相對于前兩種方法,這種方法更加簡潔明了。讓我們來看看提案文檔中提供的一個例子,該例子使用了上述3種方法實現(xiàn)了optional<T>::value()的重載集://////實現(xiàn)4次重載函數(shù)//template<typenameT>classoptional{//...constexprT&value()&{if(has_value()){returnthis->m_value;}throwbad_optional_access();}constexprTconst&value()const&{if(has_value()){returnthis->m_value;}throwbad_optional_access();}constexprT&&value()&&{if(has_value()){returnmove(this->m_value);}throwbad_optional_access();}constexprTconst&&value()const&&{if(has_value()){returnmove(this->m_value);}throwbad_optional_access();}//...};////3個重載函數(shù)委托給另外1個重載函數(shù)//template<typenameT>classoptional{//...constexprT&value()&{returnconst_cast<T&>(static_cast<optionalconst&>(*this).value());}constexprTconst&value()const&{if(has_value()){returnthis->m_value;}throwbad_optional_access();}constexprT&&value()&&{returnconst_cast<T&&>(static_cast<optionalconst&>(*this).value());}可以看到,第三種方法已經(jīng)比較接近理想的情況了,但是依然擺脫不了必須重復實現(xiàn)4個重載函數(shù)的窘境。當然了,聰明的讀者可能已經(jīng)想到了解決方法——使用一個友元非成員函數(shù)模板即可:templatetemplate<typenameT>classoptional{//...template<typenameOpt>frienddecltype(auto)value(Opt&&o){if(o.has_value()){returnforward<Opt>(o).m_value;}throwbad_optional_access();}//...};這樣的解決方案當然是可行的,不僅如此,它被使用起來也不會覺得繁瑣。但是不要忘了,我們初是打算用成員函數(shù)的方法解決成員函數(shù)的重載問題,那么現(xiàn)在是時候介紹C++23標準引入的新特性顯式對象參數(shù)了。顯式對象參數(shù)語法 \h事實上,顯式對象參數(shù)這個特性在提案之初并不叫這個名字,在提案的時候它叫做可推導this。其實在我個人看來,后者理解起來更加容易。所謂可推導this就是指this指針對象的類型是可以被推導出來的。語法形式為:聲明一個非靜態(tài)成員函數(shù)或者函數(shù)模板,其第一個參數(shù)是一個顯式對象參數(shù),用前綴關鍵可以看到上面這段代碼的第一個成員函數(shù)字this表示,具體形式為:structX{voidfoo(thisXconst&self,inti);字this表示,具體形式為:structX{voidfoo(thisXconst&self,inti);template<typenameSelf>voidbar(thisSelf&&self);};在實現(xiàn)顯式對象參數(shù)的非靜態(tài)成員函數(shù)時必須使用顯式對象參數(shù)來引用成員,例如:違反上述語法規(guī)定,MSVC會給出非常明確的錯誤提示。接下來,讓我們看看顯式對象參數(shù)是如何使用的:stru
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 催化裂化工安全培訓模擬考核試卷含答案
- 浮選藥劑工道德考核試卷含答案
- 護理行業(yè)職業(yè)生涯規(guī)劃
- 華鎣市石嶺崗110千伏輸變電新建工程報告表
- 超市銷貨合同范本
- 意外事故合同范本
- 接待劇組合同范本
- 施工購銷合同范本
- 房子寫共協(xié)議合同
- 兼職醫(yī)生合同范本
- 2025年湖南衡陽衡山縣社區(qū)專職網(wǎng)格員、警務輔助人員招聘47人筆試考試參考試題及答案解析
- 攜手并進+圓夢高考-2025-2026學年高三上學期家長會
- 西游記第86回課件
- 公司治理期末考試及答案
- 玄武門之變教學課件
- 普通高中英語課程標準(2020版vs2025日常修訂版)核心變化對照表
- 科比課件-勤奮
- 2025+急性胰腺炎護理查房
- 手足口病防治課件
- GB/T 8076-2025混凝土外加劑
- 2025年學校書香校園建設工作實施方案附件完整版:書頁翻動春天 文字生根校園
評論
0/150
提交評論