【C/C++】自家特性都做不好?- GCC的?:運(yùn)算問(wèn)題
在之前的視頻中講過(guò)GNUC的一個(gè)擴(kuò)展語(yǔ)法,“?:”表達(dá)式:https://www.bilibili.com/video/BV1Ze411K7xk
簡(jiǎn)單總結(jié)一下,就是GNUC允許“?:”運(yùn)算省略第二個(gè)運(yùn)算分量,其寫(xiě)法含義是在第一運(yùn)算分量為真時(shí),返回它自身的值,例如:
當(dāng)然,這里返回的是x自身的“原值”,而不是作為bool變量的true或false(C里面是1和0),否則就沒(méi)意義了
這種語(yǔ)法的初衷是在復(fù)雜表達(dá)式中避免重復(fù)求值,因?yàn)楸磉_(dá)式“x”可能存在副作用,例如:
當(dāng)然了,正常寫(xiě)這種代碼時(shí)候可以將表達(dá)式的值存在臨時(shí)變量中,不過(guò)那就需要增加語(yǔ)句,很多時(shí)候還是不可避免會(huì)出現(xiàn)復(fù)雜表達(dá)式的(尤其是上面舉的這個(gè)宏的例子)
在視頻中我也提到,C++中,如果我們采用返回自定義Error對(duì)象,且Error對(duì)象的布爾值表示是否有錯(cuò)誤的話,則可以利用上面的代碼簡(jiǎn)化開(kāi)發(fā),例如:
假設(shè)這個(gè)項(xiàng)目中,所有函數(shù)通過(guò)返回Err類(lèi)的智能指針來(lái)指示錯(cuò)誤,指針?lè)强毡硎境鲥e(cuò),那么顯然,上面這句代碼的含義是:依次調(diào)用xyz,如果中途某個(gè)返回錯(cuò)誤,則將其錯(cuò)誤返回,否則一直執(zhí)行到結(jié)束,這比起挨個(gè)調(diào)用并if判斷錯(cuò)誤要簡(jiǎn)單多了,代碼直觀優(yōu)雅
然而在實(shí)踐中出現(xiàn)了問(wèn)題,在最近的一個(gè)項(xiàng)目中發(fā)現(xiàn),這種代碼會(huì)導(dǎo)致程序偶現(xiàn)崩潰
看例子:
這段例程模擬了出問(wèn)題的代碼,乍一看,并有崩潰,但實(shí)際上已經(jīng)是有問(wèn)題的了,因?yàn)槲掖蛴×藄hared_ptr的內(nèi)部信息,main函數(shù)中接收的err這個(gè)指針實(shí)際是非法的(use_count是個(gè)隨機(jī)值),這也是崩潰不會(huì)必現(xiàn)的原因之一,有時(shí)候程序看似能正常運(yùn)行,但實(shí)際上內(nèi)存已經(jīng)寫(xiě)亂了
假如在f函數(shù)中不用“?:”這個(gè)語(yǔ)法,而是老老實(shí)實(shí)一個(gè)個(gè)取返回值判斷,則不會(huì)出現(xiàn)上面的問(wèn)題
為進(jìn)一步探查這個(gè)問(wèn)題,就再簡(jiǎn)化一下測(cè)試case:
通過(guò)這個(gè)例子我們看到,流程的確出了問(wèn)題,A這個(gè)對(duì)象只構(gòu)造了一次,卻有兩次析構(gòu)調(diào)用,同樣的,如果f中不用“?:”這種語(yǔ)法,而是用普通的C++語(yǔ)法來(lái)實(shí)現(xiàn),就不會(huì)有這個(gè)問(wèn)題了,進(jìn)一步做實(shí)驗(yàn)還可以得知,如果“?:”的第一運(yùn)算分量不是臨時(shí)量,則也不會(huì)出現(xiàn)問(wèn)題,所以簡(jiǎn)單總結(jié)下,這個(gè)問(wèn)題的詳細(xì)描述是:
return語(yǔ)句中用“?:”表達(dá)式,第一分量是臨時(shí)值,且布爾值為真
第二分量為空,此時(shí)預(yù)期返回第一分量的值
第一分量在表達(dá)式結(jié)束后析構(gòu)了
return出去的臨時(shí)值在調(diào)用者看來(lái)是已經(jīng)構(gòu)造的,所以調(diào)用者又調(diào)用了一次析構(gòu)
事實(shí)上,return中就算返回的不是“?:”的結(jié)果,而僅僅是包含了“?:”,也會(huì)出問(wèn)題,例如將上面f的那句改成:
那么整個(gè)程序會(huì)對(duì)A構(gòu)造兩次,析構(gòu)三次,還是有問(wèn)題,這就有點(diǎn)詭異了,因?yàn)檫@里逗號(hào)表達(dá)式的值只是最后一個(gè)子表達(dá)式(“A()”)的值,前面的應(yīng)該求值扔掉,但是這里處理依然有問(wèn)題,同樣的,如果不用“?:”這個(gè)特殊語(yǔ)法,問(wèn)題就不會(huì)出現(xiàn)
現(xiàn)象講清楚了,先說(shuō)結(jié)論,我認(rèn)為是gcc在這個(gè)GNUC擴(kuò)展語(yǔ)法的處理存在問(wèn)題
一般來(lái)說(shuō),GNUC不可能顯式規(guī)定這里就應(yīng)該多析構(gòu)一次(這不合邏輯),那么有沒(méi)有可能是未定義行為或未指定行為呢?這好像還真有可能,因?yàn)镚NUC對(duì)于“?:”這個(gè)特殊語(yǔ)法的描述,是在其C擴(kuò)展語(yǔ)法的文檔中:https://gcc.gnu.org/onlinedocs/gcc-13.1.0/gcc/Conditionals.html
至于這個(gè)語(yǔ)法能不能用在C++中,按文檔慣例似乎又是可以的(GNUC的文檔中,C擴(kuò)展如果不能應(yīng)用于C++一般會(huì)特殊說(shuō)明,例如C風(fēng)格的閉包定義),但畢竟人家沒(méi)明說(shuō)
但是,即便是這樣,我依然認(rèn)為gcc這里的處理存在問(wèn)題,因?yàn)榫瓦@個(gè)擴(kuò)展語(yǔ)法而言,可以很自然地?cái)U(kuò)展到C++,畢竟C++和C一樣,也是個(gè)基于值類(lèi)型的語(yǔ)言,只不過(guò)其值傳遞過(guò)程涉及了對(duì)象的拷貝和移動(dòng)構(gòu)造罷了,原則沒(méi)有變。雖然我之前講過(guò)C++有很多難以理解的UB,但細(xì)究起來(lái),那些東西都是在代碼優(yōu)化這一個(gè)大原則下,而上面說(shuō)的這個(gè)問(wèn)題,如果作為UB,又看不出其在優(yōu)化領(lǐng)域能有多大的作用
另一個(gè)理由是,GNUC作為一套標(biāo)準(zhǔn),并非是gcc這個(gè)編譯器集合獨(dú)占的,其他一些編譯器也實(shí)現(xiàn)或部分支持了GNUC擴(kuò)展語(yǔ)法,我們不妨看看clang是怎么處理上面的兩個(gè)例子:
可以看到,對(duì)于簡(jiǎn)化后的例子,clang的處理就是正確的,“A()?:A()”這個(gè)表達(dá)式中,在第一分量會(huì)被移動(dòng)構(gòu)造為一個(gè)新對(duì)象返回,然后析構(gòu),返回的對(duì)象在main中就可以安全析構(gòu)了,而在“shared_ptr<Err>”的例子中,clang的這種處理自然也保證了最終err指針的合法(use_count為1,符合預(yù)期),因而不會(huì)出現(xiàn)gcc編譯結(jié)果的崩潰情況。clang在這個(gè)語(yǔ)法上符合我們?cè)贑++中對(duì)其的作用推論和預(yù)期
盡管GNUC擴(kuò)展語(yǔ)法是GNU標(biāo)準(zhǔn)的,而gcc是其“正統(tǒng)”實(shí)現(xiàn),但并不代表它沒(méi)有問(wèn)題。在使用一些冷門(mén)語(yǔ)法尤其是擴(kuò)展語(yǔ)法的時(shí)候,還是要小心些