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