那些值得使用的標(biāo)準(zhǔn)?Attributes
時(shí)間:2021-09-03 10:07:59
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]今天這篇文章,我想跟大家探索下Attributes這個(gè)概念。如果你還沒(méi)有聽(tīng)過(guò)這個(gè)概念,或是一知半解,沒(méi)咋用過(guò),那正好表明它處于一個(gè)被忽略或是低估的位置。MeetingC曾經(jīng)對(duì)此做過(guò)一份調(diào)查,結(jié)果如下:From?MeetingCCommunity可以看出,大概一千人填寫(xiě)了這份問(wèn)卷,...
今天這篇文章,我想跟大家探索下Attributes這個(gè)概念。
如果你還沒(méi)有聽(tīng)過(guò)這個(gè)概念,或是一知半解,沒(méi)咋用過(guò),那正好表明它處于一個(gè)被忽略或是低估的位置。
Meeting C 曾經(jīng)對(duì)此做過(guò)一份調(diào)查,結(jié)果如下:From?Meeting C Community可以看出,大概一千人填寫(xiě)了這份問(wèn)卷,其中就有半數(shù)人表示從未用過(guò)Attributes,在被使用的Attributes當(dāng)中,使用頻率也相差較大。你可能會(huì)認(rèn)為,這些特性大都是針對(duì)寫(xiě)庫(kù)或大型項(xiàng)目的人準(zhǔn)備的,它們只是針對(duì)一些特定的場(chǎng)景進(jìn)行優(yōu)化,普通開(kāi)發(fā)者幾乎用不上。然而,事實(shí)真的如此嗎?
許多C 編譯器不僅僅實(shí)現(xiàn)了語(yǔ)言的核心特性,還通過(guò)擴(kuò)展提供了一些額外特性,比如gnu提供的__attribute__,msvc提供的__declspec。編譯器可以根據(jù)這些擴(kuò)展的特性進(jìn)行一些優(yōu)化,但由于這些特性和平臺(tái)綁定,使用這些特性就會(huì)影響代碼的可移植性。
因此,C 標(biāo)準(zhǔn)從C 11就開(kāi)始把一些有用的擴(kuò)展,慢慢添加到標(biāo)準(zhǔn)中來(lái)。這些添加進(jìn)來(lái)的擴(kuò)展就叫做「C Attributes」,標(biāo)準(zhǔn)對(duì)語(yǔ)法進(jìn)行了統(tǒng)一,使用[[attr]]或是[[namespace::attr]]來(lái)指定普通的或是帶有命名空間的Attributes。那么為什么要采用新語(yǔ)法,而非引入新的關(guān)鍵字呢?一是可以降低Attributes加入的障礙,二是可以防止關(guān)鍵字泛濫。我們的大腦在處理事務(wù)時(shí),是需要區(qū)分「背景」跟「主體」的,若所有的Attributes都被定為關(guān)鍵字,那么勢(shì)必引發(fā)關(guān)鍵字泛濫。當(dāng)一切都成為了主體,就相當(dāng)于一切都是背景,突出不了重點(diǎn)。
打個(gè)比方:在一個(gè)RPG游戲中,包含許多劇情,這些劇情不能整體都非常平淡,也不能整體都是高潮。因?yàn)槲覀儗?duì)于這個(gè)游戲的整體記憶,取決于它劇情高潮和結(jié)尾時(shí)的體驗(yàn)。劇情越是跌宕起伏、有高有低,越能夠給玩家留下深刻記憶,玩家也就越會(huì)傾向于評(píng)價(jià)這個(gè)游戲好玩。
游戲里的這些重點(diǎn)劇情就是「主體」,過(guò)渡劇情就是「背景」。背景是為主體服務(wù)的,去除它并不會(huì)影響整個(gè)劇情。同樣,是否使用Attributes也并不會(huì)影響程序的語(yǔ)義,也就是說(shuō),即使編譯器忽略一個(gè)Attribute也完全沒(méi)有壞處。順便一提,在早些時(shí)候,override和virtual本來(lái)是作為Attributes引入的,后來(lái)發(fā)現(xiàn)語(yǔ)法又丑又極易被濫用,遂改為表示語(yǔ)言特性的關(guān)鍵字,而不是注解作用的屬性。
OCW屬性模型這里跟大家介紹一套比較有用的屬性工具:「OCW屬性模型」。這是我自創(chuàng)的一個(gè)模型,它可以幫助你更好地理解、記憶跟使用Attributes。它包含了三部分,表示Attributes的三方面意義:
第二部分,約束。有句名言叫,「設(shè)計(jì)是為了厲行約束」。好的設(shè)計(jì)應(yīng)該盡可能在編譯期就發(fā)現(xiàn)大部分錯(cuò)誤,約束就是保證用戶的使用方式與你的設(shè)計(jì)意圖相符合,一些Attributes提供了這方面的能力。例如,最流行的[[nodiscard]]可以在用戶忽略重要的函數(shù)返回值時(shí),進(jìn)行提醒。[[deprecated]]可以標(biāo)記某個(gè)組件已被棄用,并告知用戶新的替代品。
第三部分,警告。C 包含許多奇技淫巧,所以有些代碼看似無(wú)用,其實(shí)不然。然而編譯期會(huì)對(duì)這些有意的代碼進(jìn)行誤判,給出警告,當(dāng)然也有技巧去消除這些警告,但Attributes提供了更加規(guī)范統(tǒng)一的做法。例如,[[fallthrough]]可以消除有意落空的case語(yǔ)句,就是故意省掉case中的break所導(dǎo)致的錯(cuò)誤。[[maybe_unused]]可以消除未使用的變量警告。[[noreturn]]可以解決「調(diào)用不會(huì)返回的函數(shù)時(shí)」缺少返回值的錯(cuò)誤。簡(jiǎn)而言之,Attributes涉及三個(gè)方面,優(yōu)化、約束與警告。在你編寫(xiě)代碼時(shí),若程序想要更多的提高,可以停下來(lái)思考一下:針對(duì)每塊代碼可不可以從OCW給程序提高一些性能,讓程序更加穩(wěn)定。
Optimizing?優(yōu)化在前面的調(diào)查結(jié)果中,可以看到涉及優(yōu)化的標(biāo)準(zhǔn)Attributes,使用率實(shí)在太低。
這可能有這些原因。第一,優(yōu)化往往只針對(duì)于特定的場(chǎng)景,所以注定使用場(chǎng)景不多。第二,這類Attributes牽涉的知識(shí)最是廣泛,想要正確使用本就不易,普通開(kāi)發(fā)者更不會(huì)輕易使用。第三,這方面教程資料尚顯匱乏,許多開(kāi)發(fā)者沒(méi)有意識(shí)去使用這些Attributes。下來(lái)讓我們先來(lái)對(duì)這些特性有了基本的理解。首先來(lái)看[[no_unique_address]],它使類數(shù)據(jù)成員可以擁有相同的地址。有什么用呢??jī)牲c(diǎn)作用。第一點(diǎn),也是非常重要的一點(diǎn),它為我們提供了一種創(chuàng)建「0字節(jié)基類子對(duì)象」的方式。大家都知道,在C 中,類(class, struct, union)對(duì)象至少會(huì)占有1字節(jié)的大小,即使類為空。這會(huì)導(dǎo)致那些沒(méi)有任何數(shù)據(jù)成員的類對(duì)象大小增加,比如最著名的是使用policy-based design時(shí)產(chǎn)生的額外開(kāi)銷:1struct?my_alloc?{?void*?allocate(size_t?n)?{?return?nullptr;}?};
2
3template<class?AllocPolicy>
4class?Foo?{
5private:
6????int?i;
7????AllocPolicy?alloc;
8};
這里,雖然my_alloc為空,但依舊占用了1字節(jié)大小,再加上tail padding,所以Foo的大小由4字節(jié)增加到了8字節(jié)。
面對(duì)這種情況,一種解決辦法是通過(guò)繼承來(lái)使用策略類,而不再將它們作為數(shù)據(jù)成員。代碼如下:1template<class?AllocPolicy>
2class?Foo?:?AllocPolicy?{
3private:
4????int?i;
5};
每個(gè)類對(duì)象大小至少為1,對(duì)于基類的子對(duì)象依舊適用。所以沒(méi)有任何數(shù)據(jù)成員的基類子對(duì)象并沒(méi)有必要增加派生類大小,此時(shí)基類子對(duì)象的大小為0,F(xiàn)oo的大小是4字節(jié)。注:「基類子對(duì)象」是個(gè)術(shù)語(yǔ),并不是指基類的子對(duì)象,而是指子類繼承基類時(shí),子類中所包含的基類所占的那部分內(nèi)存。
然而,這種方法有什么問(wèn)題呢?許多類并沒(méi)有被設(shè)計(jì)成一個(gè)基類,因此將它們作為基類也許并不合適。[[no_unique_address]]提供了另一種解決辦法,這種方式要更加優(yōu)雅:1template<class?AllocPolicy>
2class?Foo?{
3????int?i;
4????[[no_unique_address]]?AllocPolicy?alloc;
5};這里,基類子對(duì)象的大小為0,F(xiàn)oo的大小依舊為4。
說(shuō)完了第一點(diǎn),現(xiàn)在來(lái)說(shuō)它的第二點(diǎn)作用,是告訴編譯器可以重復(fù)利用padding bytes存儲(chǔ)其它數(shù)據(jù)。看如下代碼:1struct?my_type?{?int?i;?char?c;?};
2
3struct?foo?{
4????my_type?var;
5????char?c[3];
6};思考一下,foo的大小是多少?foo中包含my_type,my_type中int占4字節(jié),char占1字節(jié),加上tail padding的3字節(jié),共8字節(jié);它還包含一個(gè)3字節(jié)的char,所以現(xiàn)在一共占11字節(jié),于是再加上1字節(jié)的tail padding,最終一共占12字節(jié)。這里面tail padding一共增加了4字節(jié)的開(kāi)銷,其實(shí)my_type的那3字節(jié)開(kāi)銷,可以給3字節(jié)的char使用,這樣總共就只需要占用8字節(jié)。[[no_unique_address]]可以實(shí)現(xiàn)這個(gè)目標(biāo),代碼如下:1struct?foo?{
2????[[no_unique_address]]?my_type?var;
3????char?c[3];
4};不過(guò)就我測(cè)試,gcc和msvc似乎都還沒(méi)有這種優(yōu)化,msvc甚至第一點(diǎn)作用也不支持。另外,這里還需要強(qiáng)調(diào)一下,[[no_unique_address]]只能應(yīng)用于「非靜態(tài)的數(shù)據(jù)成員」,所以不要試圖在靜態(tài)變量或是全局變量之上使用它。。接下來(lái),簡(jiǎn)單說(shuō)下[[(un)likely]]和[[carries_dependency]],由于這兩個(gè)我打算單獨(dú)寫(xiě)文章,所以這里只蜻蜓點(diǎn)水一下。[[(un)likely]]其實(shí)包含兩個(gè):[[unlikely]]和[[likely]],用于在分支代碼中輔助編譯器實(shí)現(xiàn)更加準(zhǔn)確的「分支預(yù)測(cè)」。這到底有沒(méi)有用呢?對(duì)性能提升有多大用呢?等我準(zhǔn)備好資料數(shù)據(jù)單篇中來(lái)論。
[[carries_dependency]]這個(gè)是關(guān)于并發(fā)的優(yōu)化,涉及我們講過(guò)的Memory Order,還是單篇來(lái)說(shuō)。
總之,優(yōu)化這部分的Attributes的確有用,而且必不可少,在合適的場(chǎng)景還是推薦使用。
Constraints 約束
涉及約束的標(biāo)準(zhǔn)Attributes,只需小做努力,程序就能獲得不錯(cuò)的安全性,以及更加清晰地表明接口的真實(shí)意圖。
因而這成了使用率最高的一類Attributes。
其中[[nodiscard]]無(wú)疑又是最常用的,它的目的在于顯式地表達(dá)所定義接口的意義。可以用它來(lái)標(biāo)記一個(gè)函數(shù)的返回值:1[[nodiscard]]?int?foo()?{
2????return?1;???????
3}
4
5void?g()?{
6????foo();
7}那么當(dāng)你在調(diào)用時(shí)忽略foo()的返回值,就會(huì)引發(fā)警告,如圖。這樣的警告有效,不過(guò)很模糊,應(yīng)該使用擴(kuò)展版本[[nodiscard("reason")]]來(lái)說(shuō)明原因:1[[nodiscard("the?return?value?indicates?a?state?of?executing?result.?do?not?ignore?it.")]]?
2int?foo()?{
3????return?1;???????
4}現(xiàn)在的警告要更加友好。那么,可以在哪些地方使用它呢?這里列舉一些使用場(chǎng)景:
其次流行的是[[deprecated]],它表明棄用某個(gè)組件,組件可以是函數(shù)、變量、類等等,也可以直接棄用整個(gè)命名空間下的所有組件。
使用起來(lái)相當(dāng)簡(jiǎn)單,代碼如下:1[[deprecated]]
2void?foo()?{}
3
4int?main()?{
5????foo();
6}因?yàn)轱@式指定了[[deprecated]],所以當(dāng)你試圖調(diào)用foo()時(shí),編譯器會(huì)給出警告。當(dāng)然,只是這樣,用戶可能會(huì)不明就里。所以同時(shí),你應(yīng)該說(shuō)明棄用的原因,以及替代品。這使用的是擴(kuò)展版的[[deprecated("reason")]],修改上面代碼如下:
1[[deprecated("foo()?may?be?unsafe.?Consider?using?foo_safe()?instead.")]]
2void?foo()?{}
現(xiàn)在,指定的這個(gè)原因,將會(huì)在警告時(shí)出現(xiàn)。總結(jié)一下,[[nodiscard]]比較有用,使用場(chǎng)景非常多,可以一定程度杜絕用戶的錯(cuò)誤行為;[[deprecated]]可以在放棄舊的接口時(shí),告訴用戶應(yīng)該使用新的接口。
Warning?警告
涉及警告的Attributes最為簡(jiǎn)單易用,所以使用率也還不錯(cuò)。其中,[[maybe_unused]]用于消除編譯器的未使用變量警告。比如,你在DEBUG時(shí)期可能會(huì)設(shè)置許多斷言來(lái)檢測(cè)錯(cuò)誤,而當(dāng)你編譯RELEASE版本時(shí),就可能會(huì)產(chǎn)生這個(gè)警告:1void?foo()?{
2????int?dummy?=?1;
3????assert(dummy?==?1);
4}使用非DEBUG模式編譯上面代碼,結(jié)果如圖。通過(guò)使用[[maybe_unused]],便可以消除這個(gè)警告:1void?foo()?{
2????[[maybe_unused]]?int?dummy?=?1;
3????assert(dummy?==?1);
4}其次,來(lái)看[[fallthrough]],它的使用場(chǎng)景在于switch-case語(yǔ)句,也非常簡(jiǎn)單。看如下代碼: 1void?test(int?state)?{
2????switch(state)?{
3????case?1:
4????????std::cout?<"1\n";
5????????//?沒(méi)寫(xiě)break;
6????case?2:
7????????std::cout?<"2\n";
8????????break;
9????default:
10????????break;
11????}
12}
因?yàn)闆](méi)寫(xiě)break,編譯器會(huì)給予提醒。但有時(shí)我們是有意落空,例如: 1void?foo(int?connState)?{
2????switch(connState)?{
3????default:
4????????if(connection_timeout())?{?//?如果連接超時(shí)
5????????????connState?=?reset_connect();???//?重置連接
6????????????[[fall_through]];???
7????????}?else?{
8????????????break;
9????????}
10????case?LISTEN:
11????????...
12????}
13}
這里有一點(diǎn)需要注意,[[fallthrough]]的下一條執(zhí)行語(yǔ)句必須得是case標(biāo)簽。
最后,有一個(gè)比較特殊的Attribute,就是[[noreturn]]。為什么說(shuō)它特殊呢?因?yàn)樗袃牲c(diǎn)作用,一是消除警告,二是優(yōu)化。
在一開(kāi)始,需要明確一個(gè)觀點(diǎn),它并不是表明函數(shù)沒(méi)有返回值,而是表明函數(shù)的控制流不會(huì)返回到調(diào)用方。我用這一段代碼來(lái)進(jìn)行講解: 1//?never?return
2void?raise()?{
3????throw?"error";
4}
5
6int?f(bool?b)?{
7????if(b)?{
8????????return?10;
9????}?else?{
10????????raise();
11????}
12}
13
14void?g()?{
15????f(true);
16????std::cout?<"that?is?impossible."?<std::endl;
17}
控制流永遠(yuǎn)不會(huì)返回到調(diào)用方,往往意味著程序遇到了錯(cuò)誤,需要終結(jié)或拋出異常。那么為何我要在警告這塊講解[[noreturn]],而不是在優(yōu)化那里呢?一個(gè)很重要的原因就是,優(yōu)化并不是[[noreturn]]存在的主要目的,試想一下,這種「永遠(yuǎn)不會(huì)返回」的情況有多常見(jiàn)?幾乎很少出現(xiàn),所以通常來(lái)說(shuō)也沒(méi)有優(yōu)化的必要。它更重要的目的在于,消除警告。看第10行代碼,因?yàn)閞aise()永遠(yuǎn)不會(huì)返回,所以else分支也就沒(méi)有必要寫(xiě)return。然而編譯器并不知曉,它發(fā)現(xiàn)存在分支沒(méi)有返回,于是給出警告:因此[[noreturn]]的作用就是告訴編譯器,這個(gè)函數(shù)永遠(yuǎn)不會(huì)返回,所以其后的任何代碼都不會(huì)得到執(zhí)行,也就自然不需要返回語(yǔ)句了。對(duì)于上述代碼,若調(diào)用f(false),那么第16行代碼永遠(yuǎn)也不可能執(zhí)行到,這種代碼尤其應(yīng)當(dāng)避免。再稍微提一下,要小心使用[[noreturn]],如果你的函數(shù)包含了一個(gè)while循環(huán),之后你卻無(wú)意識(shí)地打破了這個(gè)循環(huán),程序的行為可能會(huì)變得非常怪異。總而言之,警告這類Attributes使用起來(lái)比較簡(jiǎn)單,用處當(dāng)然也不大,但當(dāng)你遇到了上述問(wèn)題,應(yīng)該想到可以使用它們來(lái)進(jìn)行解決。
通過(guò)OCW模型,我們可以知道,Attributes主要是為了幫助編譯器勘測(cè)代碼錯(cuò)誤,提高程序性能。其中最有用的要數(shù)優(yōu)化和約束,在平時(shí)的項(xiàng)目中使用這些Attributes,可以使你的代碼意圖更加清晰,讓你更好地掌控使用者的行為。當(dāng)你有性能需求時(shí),試著去使用Attributes,這能幫助編譯器更好地優(yōu)化你的代碼,生成更加高效的程序。
- EOF -
如果你還沒(méi)有聽(tīng)過(guò)這個(gè)概念,或是一知半解,沒(méi)咋用過(guò),那正好表明它處于一個(gè)被忽略或是低估的位置。
Meeting C 曾經(jīng)對(duì)此做過(guò)一份調(diào)查,結(jié)果如下:From?Meeting C Community可以看出,大概一千人填寫(xiě)了這份問(wèn)卷,其中就有半數(shù)人表示從未用過(guò)Attributes,在被使用的Attributes當(dāng)中,使用頻率也相差較大。你可能會(huì)認(rèn)為,這些特性大都是針對(duì)寫(xiě)庫(kù)或大型項(xiàng)目的人準(zhǔn)備的,它們只是針對(duì)一些特定的場(chǎng)景進(jìn)行優(yōu)化,普通開(kāi)發(fā)者幾乎用不上。然而,事實(shí)真的如此嗎?
許多C 編譯器不僅僅實(shí)現(xiàn)了語(yǔ)言的核心特性,還通過(guò)擴(kuò)展提供了一些額外特性,比如gnu提供的__attribute__,msvc提供的__declspec。編譯器可以根據(jù)這些擴(kuò)展的特性進(jìn)行一些優(yōu)化,但由于這些特性和平臺(tái)綁定,使用這些特性就會(huì)影響代碼的可移植性。
因此,C 標(biāo)準(zhǔn)從C 11就開(kāi)始把一些有用的擴(kuò)展,慢慢添加到標(biāo)準(zhǔn)中來(lái)。這些添加進(jìn)來(lái)的擴(kuò)展就叫做「C Attributes」,標(biāo)準(zhǔn)對(duì)語(yǔ)法進(jìn)行了統(tǒng)一,使用[[attr]]或是[[namespace::attr]]來(lái)指定普通的或是帶有命名空間的Attributes。那么為什么要采用新語(yǔ)法,而非引入新的關(guān)鍵字呢?一是可以降低Attributes加入的障礙,二是可以防止關(guān)鍵字泛濫。我們的大腦在處理事務(wù)時(shí),是需要區(qū)分「背景」跟「主體」的,若所有的Attributes都被定為關(guān)鍵字,那么勢(shì)必引發(fā)關(guān)鍵字泛濫。當(dāng)一切都成為了主體,就相當(dāng)于一切都是背景,突出不了重點(diǎn)。
打個(gè)比方:在一個(gè)RPG游戲中,包含許多劇情,這些劇情不能整體都非常平淡,也不能整體都是高潮。因?yàn)槲覀儗?duì)于這個(gè)游戲的整體記憶,取決于它劇情高潮和結(jié)尾時(shí)的體驗(yàn)。劇情越是跌宕起伏、有高有低,越能夠給玩家留下深刻記憶,玩家也就越會(huì)傾向于評(píng)價(jià)這個(gè)游戲好玩。
游戲里的這些重點(diǎn)劇情就是「主體」,過(guò)渡劇情就是「背景」。背景是為主體服務(wù)的,去除它并不會(huì)影響整個(gè)劇情。同樣,是否使用Attributes也并不會(huì)影響程序的語(yǔ)義,也就是說(shuō),即使編譯器忽略一個(gè)Attribute也完全沒(méi)有壞處。順便一提,在早些時(shí)候,override和virtual本來(lái)是作為Attributes引入的,后來(lái)發(fā)現(xiàn)語(yǔ)法又丑又極易被濫用,遂改為表示語(yǔ)言特性的關(guān)鍵字,而不是注解作用的屬性。
OCW屬性模型這里跟大家介紹一套比較有用的屬性工具:「OCW屬性模型」。這是我自創(chuàng)的一個(gè)模型,它可以幫助你更好地理解、記憶跟使用Attributes。它包含了三部分,表示Attributes的三方面意義:
- Optimizing(優(yōu)化):對(duì)內(nèi)存、并發(fā)、控制這些方面進(jìn)行優(yōu)化,提高性能。
- Constraints(約束):對(duì)函數(shù)、變量、類這些代碼進(jìn)行限制,增加安全。
- Warning(警告):對(duì)有意為之的代碼產(chǎn)生的警告進(jìn)行消除,避免誤報(bào)。
第二部分,約束。有句名言叫,「設(shè)計(jì)是為了厲行約束」。好的設(shè)計(jì)應(yīng)該盡可能在編譯期就發(fā)現(xiàn)大部分錯(cuò)誤,約束就是保證用戶的使用方式與你的設(shè)計(jì)意圖相符合,一些Attributes提供了這方面的能力。例如,最流行的[[nodiscard]]可以在用戶忽略重要的函數(shù)返回值時(shí),進(jìn)行提醒。[[deprecated]]可以標(biāo)記某個(gè)組件已被棄用,并告知用戶新的替代品。
第三部分,警告。C 包含許多奇技淫巧,所以有些代碼看似無(wú)用,其實(shí)不然。然而編譯期會(huì)對(duì)這些有意的代碼進(jìn)行誤判,給出警告,當(dāng)然也有技巧去消除這些警告,但Attributes提供了更加規(guī)范統(tǒng)一的做法。例如,[[fallthrough]]可以消除有意落空的case語(yǔ)句,就是故意省掉case中的break所導(dǎo)致的錯(cuò)誤。[[maybe_unused]]可以消除未使用的變量警告。[[noreturn]]可以解決「調(diào)用不會(huì)返回的函數(shù)時(shí)」缺少返回值的錯(cuò)誤。簡(jiǎn)而言之,Attributes涉及三個(gè)方面,優(yōu)化、約束與警告。在你編寫(xiě)代碼時(shí),若程序想要更多的提高,可以停下來(lái)思考一下:針對(duì)每塊代碼可不可以從OCW給程序提高一些性能,讓程序更加穩(wěn)定。
Optimizing?優(yōu)化在前面的調(diào)查結(jié)果中,可以看到涉及優(yōu)化的標(biāo)準(zhǔn)Attributes,使用率實(shí)在太低。
這可能有這些原因。第一,優(yōu)化往往只針對(duì)于特定的場(chǎng)景,所以注定使用場(chǎng)景不多。第二,這類Attributes牽涉的知識(shí)最是廣泛,想要正確使用本就不易,普通開(kāi)發(fā)者更不會(huì)輕易使用。第三,這方面教程資料尚顯匱乏,許多開(kāi)發(fā)者沒(méi)有意識(shí)去使用這些Attributes。下來(lái)讓我們先來(lái)對(duì)這些特性有了基本的理解。首先來(lái)看[[no_unique_address]],它使類數(shù)據(jù)成員可以擁有相同的地址。有什么用呢??jī)牲c(diǎn)作用。第一點(diǎn),也是非常重要的一點(diǎn),它為我們提供了一種創(chuàng)建「0字節(jié)基類子對(duì)象」的方式。大家都知道,在C 中,類(class, struct, union)對(duì)象至少會(huì)占有1字節(jié)的大小,即使類為空。這會(huì)導(dǎo)致那些沒(méi)有任何數(shù)據(jù)成員的類對(duì)象大小增加,比如最著名的是使用policy-based design時(shí)產(chǎn)生的額外開(kāi)銷:1struct?my_alloc?{?void*?allocate(size_t?n)?{?return?nullptr;}?};
2
3template<class?AllocPolicy>
4class?Foo?{
5private:
6????int?i;
7????AllocPolicy?alloc;
8};
這里,雖然my_alloc為空,但依舊占用了1字節(jié)大小,再加上tail padding,所以Foo的大小由4字節(jié)增加到了8字節(jié)。
面對(duì)這種情況,一種解決辦法是通過(guò)繼承來(lái)使用策略類,而不再將它們作為數(shù)據(jù)成員。代碼如下:1template<class?AllocPolicy>
2class?Foo?:?AllocPolicy?{
3private:
4????int?i;
5};
每個(gè)類對(duì)象大小至少為1,對(duì)于基類的子對(duì)象依舊適用。所以沒(méi)有任何數(shù)據(jù)成員的基類子對(duì)象并沒(méi)有必要增加派生類大小,此時(shí)基類子對(duì)象的大小為0,F(xiàn)oo的大小是4字節(jié)。注:「基類子對(duì)象」是個(gè)術(shù)語(yǔ),并不是指基類的子對(duì)象,而是指子類繼承基類時(shí),子類中所包含的基類所占的那部分內(nèi)存。
然而,這種方法有什么問(wèn)題呢?許多類并沒(méi)有被設(shè)計(jì)成一個(gè)基類,因此將它們作為基類也許并不合適。[[no_unique_address]]提供了另一種解決辦法,這種方式要更加優(yōu)雅:1template<class?AllocPolicy>
2class?Foo?{
3????int?i;
4????[[no_unique_address]]?AllocPolicy?alloc;
5};這里,基類子對(duì)象的大小為0,F(xiàn)oo的大小依舊為4。
說(shuō)完了第一點(diǎn),現(xiàn)在來(lái)說(shuō)它的第二點(diǎn)作用,是告訴編譯器可以重復(fù)利用padding bytes存儲(chǔ)其它數(shù)據(jù)。看如下代碼:1struct?my_type?{?int?i;?char?c;?};
2
3struct?foo?{
4????my_type?var;
5????char?c[3];
6};思考一下,foo的大小是多少?foo中包含my_type,my_type中int占4字節(jié),char占1字節(jié),加上tail padding的3字節(jié),共8字節(jié);它還包含一個(gè)3字節(jié)的char,所以現(xiàn)在一共占11字節(jié),于是再加上1字節(jié)的tail padding,最終一共占12字節(jié)。這里面tail padding一共增加了4字節(jié)的開(kāi)銷,其實(shí)my_type的那3字節(jié)開(kāi)銷,可以給3字節(jié)的char使用,這樣總共就只需要占用8字節(jié)。[[no_unique_address]]可以實(shí)現(xiàn)這個(gè)目標(biāo),代碼如下:1struct?foo?{
2????[[no_unique_address]]?my_type?var;
3????char?c[3];
4};不過(guò)就我測(cè)試,gcc和msvc似乎都還沒(méi)有這種優(yōu)化,msvc甚至第一點(diǎn)作用也不支持。另外,這里還需要強(qiáng)調(diào)一下,[[no_unique_address]]只能應(yīng)用于「非靜態(tài)的數(shù)據(jù)成員」,所以不要試圖在靜態(tài)變量或是全局變量之上使用它。。接下來(lái),簡(jiǎn)單說(shuō)下[[(un)likely]]和[[carries_dependency]],由于這兩個(gè)我打算單獨(dú)寫(xiě)文章,所以這里只蜻蜓點(diǎn)水一下。[[(un)likely]]其實(shí)包含兩個(gè):[[unlikely]]和[[likely]],用于在分支代碼中輔助編譯器實(shí)現(xiàn)更加準(zhǔn)確的「分支預(yù)測(cè)」。這到底有沒(méi)有用呢?對(duì)性能提升有多大用呢?等我準(zhǔn)備好資料數(shù)據(jù)單篇中來(lái)論。
[[carries_dependency]]這個(gè)是關(guān)于并發(fā)的優(yōu)化,涉及我們講過(guò)的Memory Order,還是單篇來(lái)說(shuō)。
總之,優(yōu)化這部分的Attributes的確有用,而且必不可少,在合適的場(chǎng)景還是推薦使用。
Constraints 約束
涉及約束的標(biāo)準(zhǔn)Attributes,只需小做努力,程序就能獲得不錯(cuò)的安全性,以及更加清晰地表明接口的真實(shí)意圖。
因而這成了使用率最高的一類Attributes。
其中[[nodiscard]]無(wú)疑又是最常用的,它的目的在于顯式地表達(dá)所定義接口的意義。可以用它來(lái)標(biāo)記一個(gè)函數(shù)的返回值:1[[nodiscard]]?int?foo()?{
2????return?1;???????
3}
4
5void?g()?{
6????foo();
7}那么當(dāng)你在調(diào)用時(shí)忽略foo()的返回值,就會(huì)引發(fā)警告,如圖。這樣的警告有效,不過(guò)很模糊,應(yīng)該使用擴(kuò)展版本[[nodiscard("reason")]]來(lái)說(shuō)明原因:1[[nodiscard("the?return?value?indicates?a?state?of?executing?result.?do?not?ignore?it.")]]?
2int?foo()?{
3????return?1;???????
4}現(xiàn)在的警告要更加友好。那么,可以在哪些地方使用它呢?這里列舉一些使用場(chǎng)景:
- 錯(cuò)誤信息,error_code
- 工廠方法(若使用智能指針則沒(méi)必要),因?yàn)槠渲袝?huì)使用malloc/new這類函數(shù)分配內(nèi)存。
- 返回的信息有特定作用,用戶應(yīng)該處理。即返回的是non-trivial類型,用戶不處理可能引發(fā)資源泄漏之類的問(wèn)題。
- 不使用返回值,通常會(huì)出現(xiàn)錯(cuò)誤
- std::async(),不使用返回值會(huì)導(dǎo)致調(diào)用變成同步的
其次流行的是[[deprecated]],它表明棄用某個(gè)組件,組件可以是函數(shù)、變量、類等等,也可以直接棄用整個(gè)命名空間下的所有組件。
使用起來(lái)相當(dāng)簡(jiǎn)單,代碼如下:1[[deprecated]]
2void?foo()?{}
3
4int?main()?{
5????foo();
6}因?yàn)轱@式指定了[[deprecated]],所以當(dāng)你試圖調(diào)用foo()時(shí),編譯器會(huì)給出警告。當(dāng)然,只是這樣,用戶可能會(huì)不明就里。所以同時(shí),你應(yīng)該說(shuō)明棄用的原因,以及替代品。這使用的是擴(kuò)展版的[[deprecated("reason")]],修改上面代碼如下:
1[[deprecated("foo()?may?be?unsafe.?Consider?using?foo_safe()?instead.")]]
2void?foo()?{}
現(xiàn)在,指定的這個(gè)原因,將會(huì)在警告時(shí)出現(xiàn)。總結(jié)一下,[[nodiscard]]比較有用,使用場(chǎng)景非常多,可以一定程度杜絕用戶的錯(cuò)誤行為;[[deprecated]]可以在放棄舊的接口時(shí),告訴用戶應(yīng)該使用新的接口。
Warning?警告
涉及警告的Attributes最為簡(jiǎn)單易用,所以使用率也還不錯(cuò)。其中,[[maybe_unused]]用于消除編譯器的未使用變量警告。比如,你在DEBUG時(shí)期可能會(huì)設(shè)置許多斷言來(lái)檢測(cè)錯(cuò)誤,而當(dāng)你編譯RELEASE版本時(shí),就可能會(huì)產(chǎn)生這個(gè)警告:1void?foo()?{
2????int?dummy?=?1;
3????assert(dummy?==?1);
4}使用非DEBUG模式編譯上面代碼,結(jié)果如圖。通過(guò)使用[[maybe_unused]],便可以消除這個(gè)警告:1void?foo()?{
2????[[maybe_unused]]?int?dummy?=?1;
3????assert(dummy?==?1);
4}其次,來(lái)看[[fallthrough]],它的使用場(chǎng)景在于switch-case語(yǔ)句,也非常簡(jiǎn)單。看如下代碼: 1void?test(int?state)?{
2????switch(state)?{
3????case?1:
4????????std::cout?<"1\n";
5????????//?沒(méi)寫(xiě)break;
6????case?2:
7????????std::cout?<"2\n";
8????????break;
9????default:
10????????break;
11????}
12}
因?yàn)闆](méi)寫(xiě)break,編譯器會(huì)給予提醒。但有時(shí)我們是有意落空,例如: 1void?foo(int?connState)?{
2????switch(connState)?{
3????default:
4????????if(connection_timeout())?{?//?如果連接超時(shí)
5????????????connState?=?reset_connect();???//?重置連接
6????????????[[fall_through]];???
7????????}?else?{
8????????????break;
9????????}
10????case?LISTEN:
11????????...
12????}
13}
這里有一點(diǎn)需要注意,[[fallthrough]]的下一條執(zhí)行語(yǔ)句必須得是case標(biāo)簽。
最后,有一個(gè)比較特殊的Attribute,就是[[noreturn]]。為什么說(shuō)它特殊呢?因?yàn)樗袃牲c(diǎn)作用,一是消除警告,二是優(yōu)化。
在一開(kāi)始,需要明確一個(gè)觀點(diǎn),它并不是表明函數(shù)沒(méi)有返回值,而是表明函數(shù)的控制流不會(huì)返回到調(diào)用方。我用這一段代碼來(lái)進(jìn)行講解: 1//?never?return
2void?raise()?{
3????throw?"error";
4}
5
6int?f(bool?b)?{
7????if(b)?{
8????????return?10;
9????}?else?{
10????????raise();
11????}
12}
13
14void?g()?{
15????f(true);
16????std::cout?<"that?is?impossible."?<std::endl;
17}
控制流永遠(yuǎn)不會(huì)返回到調(diào)用方,往往意味著程序遇到了錯(cuò)誤,需要終結(jié)或拋出異常。那么為何我要在警告這塊講解[[noreturn]],而不是在優(yōu)化那里呢?一個(gè)很重要的原因就是,優(yōu)化并不是[[noreturn]]存在的主要目的,試想一下,這種「永遠(yuǎn)不會(huì)返回」的情況有多常見(jiàn)?幾乎很少出現(xiàn),所以通常來(lái)說(shuō)也沒(méi)有優(yōu)化的必要。它更重要的目的在于,消除警告。看第10行代碼,因?yàn)閞aise()永遠(yuǎn)不會(huì)返回,所以else分支也就沒(méi)有必要寫(xiě)return。然而編譯器并不知曉,它發(fā)現(xiàn)存在分支沒(méi)有返回,于是給出警告:因此[[noreturn]]的作用就是告訴編譯器,這個(gè)函數(shù)永遠(yuǎn)不會(huì)返回,所以其后的任何代碼都不會(huì)得到執(zhí)行,也就自然不需要返回語(yǔ)句了。對(duì)于上述代碼,若調(diào)用f(false),那么第16行代碼永遠(yuǎn)也不可能執(zhí)行到,這種代碼尤其應(yīng)當(dāng)避免。再稍微提一下,要小心使用[[noreturn]],如果你的函數(shù)包含了一個(gè)while循環(huán),之后你卻無(wú)意識(shí)地打破了這個(gè)循環(huán),程序的行為可能會(huì)變得非常怪異。總而言之,警告這類Attributes使用起來(lái)比較簡(jiǎn)單,用處當(dāng)然也不大,但當(dāng)你遇到了上述問(wèn)題,應(yīng)該想到可以使用它們來(lái)進(jìn)行解決。
通過(guò)OCW模型,我們可以知道,Attributes主要是為了幫助編譯器勘測(cè)代碼錯(cuò)誤,提高程序性能。其中最有用的要數(shù)優(yōu)化和約束,在平時(shí)的項(xiàng)目中使用這些Attributes,可以使你的代碼意圖更加清晰,讓你更好地掌控使用者的行為。當(dāng)你有性能需求時(shí),試著去使用Attributes,這能幫助編譯器更好地優(yōu)化你的代碼,生成更加高效的程序。
- EOF -