談?wù)凜 新標(biāo)準(zhǔn)帶來(lái)的屬性(Attribute)
時(shí)間:2021-08-19 15:51:26
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]從C11開始,標(biāo)準(zhǔn)引入了一個(gè)新概念“屬性(attribute)”,本文將簡(jiǎn)單介紹一下目前在C標(biāo)準(zhǔn)中已經(jīng)添加的各個(gè)屬性以及常用屬性的具體應(yīng)用。?一?屬性(Attribute)的前世今生其實(shí)C早在[pre03]甚至更早的時(shí)候就已經(jīng)有了屬性的需求。彼時(shí),當(dāng)程序員需要和編譯器溝通,為某些...
從C 11開始,標(biāo)準(zhǔn)引入了一個(gè)新概念“屬性(attribute)”,本文將簡(jiǎn)單介紹一下目前在C 標(biāo)準(zhǔn)中已經(jīng)添加的各個(gè)屬性以及常用屬性的具體應(yīng)用。?
一? 屬性(Attribute)的前世今生
其實(shí)C 早在[pre03]甚至更早的時(shí)候就已經(jīng)有了屬性的需求。彼時(shí),當(dāng)程序員需要和編譯器溝通,為某些實(shí)體添加一些額外的信息的時(shí)候,為了避免“發(fā)明”一個(gè)新的關(guān)鍵詞乃至于引起一些語(yǔ)法更改的麻煩,同時(shí)又必須讓這些擴(kuò)展內(nèi)容不至于“污染”標(biāo)準(zhǔn)的命名空間,所以標(biāo)準(zhǔn)保留了一個(gè)特殊的用戶命名空間——“雙下劃線關(guān)鍵詞”,以方便各大編譯器廠商能夠根據(jù)需要添加相應(yīng)的語(yǔ)言擴(kuò)展。根據(jù)這個(gè)標(biāo)準(zhǔn),各大編譯器廠商都做出了自己的擴(kuò)展實(shí)現(xiàn),目前在業(yè)界廣泛使用的屬性空間有GNU和IBM的 __attribute__(()),微軟的 __declspec(),甚至C#還引入了獨(dú)特的單括號(hào)系統(tǒng)(single bracket system)來(lái)完成相應(yīng)的工作。
隨著編譯器和語(yǔ)言標(biāo)準(zhǔn)的發(fā)展,尤其是C 多年來(lái)也開始逐漸借鑒其他語(yǔ)言中的獨(dú)特?cái)U(kuò)展,屬性相關(guān)的擴(kuò)展也越來(lái)越龐大。但是Attribute的語(yǔ)法強(qiáng)烈依賴于各大編譯器的具體實(shí)現(xiàn),彼此之間并不兼容,甚至部分關(guān)鍵屬性導(dǎo)致了語(yǔ)言的分裂,最終都會(huì)讓使用者的無(wú)所適從。所以在C 11標(biāo)準(zhǔn)中,特意提出了C 語(yǔ)言內(nèi)置的屬性概念。提案大約是在2007年前后形成,2008年9月15日的提案版本n2761被正式接納為C 11標(biāo)準(zhǔn)中的Attribute擴(kuò)展部分(此處歷史略悠久,很可能有不準(zhǔn)確的部分,歡迎各位指正)。
二? 屬性的語(yǔ)法定義
正如我們?cè)谏弦还?jié)討論的,屬性的關(guān)鍵要求就是避免對(duì)標(biāo)準(zhǔn)用戶命名空間的污染,同時(shí)對(duì)于未來(lái)可能引入的更多屬性,我們需要有一個(gè)方式可以避免新加的“屬性關(guān)鍵字”破壞當(dāng)前已有的C 語(yǔ)法。所以新標(biāo)準(zhǔn)采用了“雙方括號(hào)”的語(yǔ)法方式引入了屬性說(shuō)明,比如[[noreturn]]就是一個(gè)標(biāo)準(zhǔn)的C 屬性定義。而未來(lái)新屬性的添加都被控制在雙方括號(hào)范圍之內(nèi),不會(huì)進(jìn)入標(biāo)準(zhǔn)的命名空間。
按照C 語(yǔ)言標(biāo)準(zhǔn),下列語(yǔ)言實(shí)體可以被屬性所定義/并從中獲益:
- 函數(shù)
- 變量
- 函數(shù)或者變量的名稱
- 類型
- 程序塊
- Translation Unit (這個(gè)不知道用中文咋說(shuō))
- 程序控制聲明
根據(jù)C 的標(biāo)準(zhǔn)提案,屬性可以出現(xiàn)在程序中的幾乎所有的位置。當(dāng)然屬性出現(xiàn)的位置和其修飾的對(duì)象是有一定關(guān)聯(lián)的,屬性僅在合適的位置才能產(chǎn)生效果。比如[[noreturn]必須出現(xiàn)在函數(shù)定義的位置才會(huì)產(chǎn)生效果,如果出現(xiàn)在某個(gè)變量的聲明處則無(wú)效。根據(jù)C 17的標(biāo)準(zhǔn),未實(shí)現(xiàn)的或者無(wú)效的屬性均應(yīng)該被編譯器忽略且不產(chǎn)生任何錯(cuò)誤報(bào)告(在C 17標(biāo)準(zhǔn)之前的編譯器則參考編譯器的具體實(shí)現(xiàn)會(huì)有不同的行為)。
由于屬性可以出現(xiàn)在幾乎所有的位置,那么它是如何關(guān)聯(lián)到具體的作用對(duì)象呢?下面我引用了語(yǔ)言標(biāo)準(zhǔn)提案中的一個(gè)例子幫助大家理解屬性是如何作用于語(yǔ)言的各個(gè)部分。
[[attr1]] class C [[ attr2 ]] { } [[ attr3 ]] c [[ attr4 ]], d [[ attr5 ]];
- attr1 作用于class C的實(shí)體定義c和d
- attr2 作用于class C的定義?
- attr3 作用于類型C?
- attr4 作用于實(shí)體c?
- attr5 作用于實(shí)體d??
以上只是一個(gè)基本的例子,具體到實(shí)際的編程中,還有有太多的可能,如有具體情況可以參考C 語(yǔ)言標(biāo)準(zhǔn)或者編譯器的相關(guān)文檔。
三? 主流C 編譯器對(duì)于屬性的支持情況
目前的主流編譯器對(duì)于C 11的支持已經(jīng)相對(duì)很完善了,所以對(duì)于屬性的基本語(yǔ)法,大部分的編譯器都已經(jīng)能夠接納。不過(guò)對(duì)于在不同標(biāo)準(zhǔn)中引入的各個(gè)具體屬性支持則參差不齊,對(duì)于相關(guān)屬性能否發(fā)揮應(yīng)有的作用更需要具體問(wèn)題具體分析。當(dāng)然,在標(biāo)準(zhǔn)中(C 17)也明確了,對(duì)于不支持或者錯(cuò)誤設(shè)定的屬性,編譯器也能夠忽略不會(huì)報(bào)錯(cuò)。
下圖是目前主流編譯器對(duì)于n2761屬性提案的支持情況:
對(duì)于未知或不支持的屬性忽略報(bào)錯(cuò)的主流編譯器支持情況:
四? 目前C 標(biāo)準(zhǔn)中引入的標(biāo)準(zhǔn)屬性
C 11引入標(biāo)準(zhǔn):
- [[noreturn]]
- [[carries_dependency]]
C 14引入標(biāo)準(zhǔn):
- [[deprecated]] 和 [[deprecated("reason")]]
C 17引入標(biāo)準(zhǔn):
- [[fallthrough]]
- [[nodiscard]] 和 [[nodiscard("reason")]] (C 20)
- [[maybe_unused]]
C 20引入標(biāo)準(zhǔn):
- [[likely]] 和 [[unlikely]]
- [[no_unique_address]]
接下來(lái)我將嘗試對(duì)已經(jīng)引入標(biāo)準(zhǔn)的屬性進(jìn)行進(jìn)一步的說(shuō)明,同時(shí)對(duì)于已經(jīng)明確得到編譯器支持的屬性,我也會(huì)嘗試用例子進(jìn)行進(jìn)一步的探索,希望拋磚引玉能夠幫大家更好的使用C 屬性這個(gè)“新的老朋友”。
1? [[noreturn]]
從字面意義上來(lái)看,noreturn是非常容易理解的,這個(gè)屬性的含義就是標(biāo)明某個(gè)函數(shù)一定不會(huì)返回。
請(qǐng)看下面的例子程序:
// 正確,函數(shù)將永遠(yuǎn)不會(huì)返回。
[[noreturn]] void func1()
{?throw?"error";?}
// 錯(cuò)誤,如果用false進(jìn)行調(diào)用,函數(shù)是會(huì)返回的,這時(shí)候會(huì)導(dǎo)致未定義行為。
[[noreturn]] void func2(bool b)
{?if?(b)?throw?"error";?}
int main()
{
try
{?func1()??;?}
catch(char const *e)
{?std::cout?<"Got?something:?"?<"??\n";?}
// 此處編譯會(huì)有警告信息。
func2(false);
}
這個(gè)屬性最容易被誤解的地方是返回值為void的函數(shù)不代表著不會(huì)返回,它只是沒有返回值而已。所以在例子中的第一個(gè)函數(shù)func1才是正確的無(wú)返回函數(shù)的一個(gè)例子;而func2在參數(shù)值為false的情況下,它還是一個(gè)會(huì)返回的函數(shù)。所以,在編譯的時(shí)候,編譯器會(huì)針對(duì)func2報(bào)告如下錯(cuò)誤:
noreturn.cpp: In function 'void func2(bool)':
noreturn.cpp:11:1: warning: 'noreturn' function does return
11 | }
| ^
而實(shí)際運(yùn)行的時(shí)候,func2到底會(huì)有什么樣的表現(xiàn)屬于典型的“未定義行為”,程序可能崩潰也可能什么都不發(fā)生,所以一定要避免這種情況在我們的代碼中出現(xiàn)。(我在gcc11編譯器環(huán)境下嘗試過(guò)幾次,情況是什么都不發(fā)生,但是無(wú)法保證這是確定的行為。)
另外,[[noreturn]]只要函數(shù)最終沒有返回都是可以的,比如用exit()調(diào)用直接將程序干掉的程序也是可以被編譯器接受的行為(只是暫時(shí)沒想到為啥要這么干)。
2? [[carries_dependency]]
這個(gè)屬性的作用是允許我們將dependency跨越函數(shù)進(jìn)行傳遞,用于避免在弱一致性模型平臺(tái)上產(chǎn)生不必要的內(nèi)存柵欄導(dǎo)致代碼效率降低。
一般來(lái)說(shuō),這個(gè)屬性是搭配 std::memory_order_consume 來(lái)使用的,支持這個(gè)屬性的編譯器可以根據(jù)屬性的指示生成更合適的代碼幫助程序在線程之間傳遞數(shù)據(jù)。在典型的情況下,如果在 memory_order_consume 的情況下讀取一個(gè)值,編譯器為了保證合適的內(nèi)存讀取順序,可能需要額外的內(nèi)存柵欄協(xié)調(diào)程序行為順序,但是如果加上了[[carries_dependency]]的屬性,則編譯器可以保證函數(shù)體也被擴(kuò)展包含了同樣的dependency,從而不再需要這個(gè)額外的內(nèi)存柵欄。同樣的事情對(duì)于函數(shù)的返回值也是一致的。
參考如下例子代碼:
std::atomic<int *> p;
std::atomic<int?*>?q;
void func1(int *val)
{?std::cout?<*val?<std::endl;?}
void func2(int * [[carries_dependency]] val)
{ q.store(val, std::memory_order_release);
std::cout?<*q?<std::endl;?}
void thread_job()
{
int *ptr1 = (int *)p.load(std::memory_order_consume); // 1
std::cout << *ptr1 << std::endl; // 2
func1(ptr1); // 3
func2(ptr1); // 4
}
- 程序在1的位置因?yàn)閜tr1明確的使用了memory_order_consume的內(nèi)存策略,所以對(duì)于ptr1的訪問(wèn)一定會(huì)被編譯器排到這一行之后。
- 因?yàn)?的原因,所以這一行在編譯的時(shí)候勢(shì)必會(huì)排列在1后面。
- func1并沒有帶任何屬性,而他訪問(wèn)了ptr1,那么編譯器為了保證內(nèi)存訪問(wèn)策略被尊重所以必須在func1調(diào)用之間構(gòu)建一個(gè)內(nèi)存柵欄。如果這個(gè)線程被大量的調(diào)用,這個(gè)額外的內(nèi)存柵欄將導(dǎo)致性能損失。
- 在func2中,我們使用了[[carries_dependency]]屬性,那么同樣的訪問(wèn)ptr1,編譯器就知道程序已經(jīng)處理好了相關(guān)的內(nèi)存訪問(wèn)限制。這個(gè)也正如我們?cè)賔unc2中對(duì)val訪問(wèn)所做的限制是一樣的。那么在func2之前,編譯器就無(wú)需再插入額外的內(nèi)存柵欄,提高了效率。
3? [[deprecated]] 和 [[deprecated("reason")]]
這個(gè)屬性是在C 14的標(biāo)準(zhǔn)中被引入的。被這個(gè)屬性加持的名稱或者實(shí)體在編譯期間會(huì)輸出對(duì)應(yīng)的警告,告訴使用者該名稱或者實(shí)體將在未來(lái)被拋棄。如果指定了具體的"reason",則這個(gè)具體的原因也會(huì)被包含在警告信息中。?
參考如下例子程序:
[[deprecated]]
void old_hello() {}
[[deprecated("Use new_greeting() instead. ")]]
void old_greeting() {}
int main()
{
old_hello();
old_greeting();
return 0;
}
在支持對(duì)應(yīng)屬性的編譯器上,這個(gè)例子程序是可以通過(guò)編譯并正確運(yùn)行的,但是編譯的過(guò)程中,編譯器會(huì)對(duì)屬性標(biāo)志的函數(shù)進(jìn)行追蹤,并且打印出相應(yīng)的信息(如果定義了的話)。在我的環(huán)境中,編譯程序給出了我如下的提示信息:
deprecated.cpp: In function 'int main()':
deprecated.cpp:9:14: warning: 'void old_hello()' is deprecated [-Wdeprecated-declarations]
9 | old_hello();
| ~~~~~~~~~^~
deprecated.cpp:2:6: note: declared here
2 | void old_hello() {}
| ^~~~~~~~~
deprecated.cpp:10:17: warning: 'void old_greeting()' is deprecated:
Use new_greeting() instead. [-Wdeprecated-declarations]
10 | old_greeting();
| ~~~~~~~~~~~~^~
deprecated.cpp:5:6: note: declared here
5 | void old_greeting() {}
| ^~~~~~~~~~~~
[[deprecated]]屬性支持廣泛的名字和實(shí)體,除了函數(shù),它還可以修飾:
- 類,結(jié)構(gòu)體
- 靜態(tài)數(shù)據(jù)成員,非靜態(tài)數(shù)據(jù)成員
- 聯(lián)合體,枚舉,枚舉項(xiàng)
- 變量,別名,命名空間
- 模板特化
4? [[fallthrough]]
這個(gè)屬性只可以用于switch語(yǔ)句中,通常在case處理完畢之后需要按照程序設(shè)定的邏輯退出switch塊,通常是添加break語(yǔ)句;或者在某些時(shí)候,程序又需要直接進(jìn)入下一個(gè)case的判斷中。而現(xiàn)代編譯器通常會(huì)檢測(cè)程序邏輯,在前一個(gè)case處理完畢不添加break的情況下發(fā)出一個(gè)警告信息,讓作者確定是否是他的真實(shí)意圖。但是,在case處理部分添加了[[fallthrough]]屬性之后,編譯器就知道這是程序邏輯有意為之,而不再給出提示信息。
5? [[nodiscard]] 和 [[nodiscard("reason")]]
這兩個(gè)屬性和前面的[[deprecated]]類似,但是他們是在不同的C 標(biāo)準(zhǔn)中被引入的,[[nodiscard]]是在C 17標(biāo)準(zhǔn)中引入,而[[nodiscard("reason")]]是在C 20標(biāo)準(zhǔn)中引入。
這個(gè)屬性的含義是明確的告訴編譯器,用此屬性修飾的函數(shù),其返回值(必須是按值返回)不應(yīng)該被丟棄,如果在實(shí)際調(diào)用中舍棄了返回變量,則編譯器會(huì)發(fā)出警示信息。如果此屬性修飾的是枚舉或者類,則在對(duì)應(yīng)函數(shù)返回該類型的時(shí)候也不應(yīng)該丟棄結(jié)果。
參考下面的例子程序:
struct [[nodiscard("IMPORTANT THING")]] important {};
important i = important();
important get_important() { return i; }
important