欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

c++臨時(shí)對(duì)象導(dǎo)致的生命周期問題

 更新時(shí)間:2024年07月10日 09:44:02   作者:apocelipes  
對(duì)象的生命周期是c++中非常重要的概念,它直接決定了你的程序是否正確以及是否存在安全問題,這篇文章主要介紹了c++臨時(shí)對(duì)象導(dǎo)致的生命周期問題 ,需要的朋友可以參考下

對(duì)象的生命周期是c++中非常重要的概念,它直接決定了你的程序是否正確以及是否存在安全問題。

今天要說(shuō)的臨時(shí)變量導(dǎo)致的生命周期問題是非常常見的,很多時(shí)候沒有一定經(jīng)驗(yàn)甚至沒法識(shí)別出來(lái)。光是我自己寫、review、回答別人的問題就犯了或者看到了許許多多這類問題,所以我想有必要做個(gè)簡(jiǎn)單的總結(jié),自己備忘的同時(shí)也盡量幫其他開發(fā)者尤其是別的語(yǔ)言轉(zhuǎn)c++的人少踩些坑。

問題主要分為三類,每類我都會(huì)給出典型例子,最后會(huì)給出解決辦法。不過在深入討論每一類問題之前,先讓我們復(fù)習(xí)點(diǎn)必要的基礎(chǔ)知識(shí)。

基礎(chǔ)回顧

基礎(chǔ)回顧少不了,否則看c++的文章容易變成看天書。

但也別緊張,都叫“基礎(chǔ)”了那肯定是些簡(jiǎn)單的偏常識(shí)的東西,不難的。

第一個(gè)基礎(chǔ)是語(yǔ)句和表達(dá)式。語(yǔ)句好理解,for(...){}是一個(gè)語(yǔ)句,int a = num + 1;也是一個(gè)語(yǔ)句,除了一些特殊的語(yǔ)法結(jié)構(gòu),語(yǔ)句通常以分號(hào)結(jié)尾。表達(dá)式是什么呢,語(yǔ)句中除了關(guān)鍵字和符號(hào)之外的東西都可以算表達(dá)式,比如int a = num + 1中,num、1、num + 1都是表達(dá)式。當(dāng)然單獨(dú)的表達(dá)式也可以構(gòu)成語(yǔ)句,比如num;是語(yǔ)句。

這里就有個(gè)概率要回顧了:“完整的表達(dá)式”。什么叫完整,粗暴的理解就是同一個(gè)語(yǔ)句里的所有子表達(dá)式組合起來(lái)的那個(gè)表達(dá)式才叫“完整的表達(dá)式”。舉個(gè)例子int a = num + 1;int a = num + 1才是一個(gè)完整的表達(dá)式;str().trimmed().replace(pattern, gettext());str().trimmed().replace(pattern, gettext())才是完整的表達(dá)式。

這個(gè)概念后面會(huì)很有用。

第二個(gè)要復(fù)習(xí)的是const T &對(duì)臨時(shí)變量生命周期的影響。

一個(gè)臨時(shí)對(duì)象(通常是prvalue)可以綁定到const T &或者右值引用上。綁定后臨時(shí)對(duì)象的生命周期會(huì)一直延長(zhǎng)到綁定的引用的生命周期結(jié)束的時(shí)候。但延長(zhǎng)有一個(gè)例外:

const int &func()
{
    return 100;
}

這個(gè)大家都知道是懸垂引用,但const T &不是能延長(zhǎng)100這個(gè)臨時(shí)int對(duì)象的生命周期嗎,這里理論上不應(yīng)該是和返回值的生命周期一樣么,這么會(huì)變成懸垂引用?

答案是語(yǔ)法規(guī)定的例外,引用綁定延長(zhǎng)的生命周期不能跨越作用域。這里顯然100是在函數(shù)內(nèi)的作用域,而返回的引用作用域在函數(shù)之外,跨越作用域了,所以這時(shí)綁定不能延長(zhǎng)臨時(shí)int對(duì)象的生命周期,臨時(shí)對(duì)象在函數(shù)調(diào)用結(jié)束后銷毀,所以產(chǎn)生了懸垂引用。

另外綁定帶來(lái)的延長(zhǎng)是不能傳遞的,只有直接綁定到臨時(shí)對(duì)象上才能延長(zhǎng)生命,其他情況比如通過另一個(gè)引用進(jìn)行的綁定都沒有效果。

復(fù)習(xí)到此為止,我們來(lái)看具體問題。

函數(shù)調(diào)用中的生命周期問題

先看例子:

const int &value = std::max(v, 100);

這是三類問題中最常見的一類,甚至常見到了各大文檔包括cppreference上都專門開了個(gè)腳注告訴你這么寫是錯(cuò)的。

這個(gè)錯(cuò)也很難察覺,我們一步步來(lái)。

首先是看std::max的函數(shù)簽名,當(dāng)然因?yàn)閷?shí)現(xiàn)代碼也很簡(jiǎn)單所以一塊看下簡(jiǎn)化版:

template <typename T>
const T & max(const T &a, const T &b)
{
    return a>b ? a : b;
}

參數(shù)用const T &有道理,這樣左值右值都能收;返回值用引用也還算有道理,畢竟這里復(fù)制一份參數(shù)語(yǔ)義和性能上都比較欠缺,因?yàn)槲覀円氖莂和b中最大的那個(gè),而不是最大值的副本。真正的問題是這么做之后,max的返回值不能延長(zhǎng)a或者b的生命周期,但a和b卻可以延長(zhǎng)作為參數(shù)的臨時(shí)對(duì)象的生命周期,換句話說(shuō)max只能延長(zhǎng)臨時(shí)對(duì)象的生命周期到max函數(shù)運(yùn)行結(jié)束。

現(xiàn)在還不知道問題在哪對(duì)吧,我們接著看std::max(v, 100)這個(gè)表達(dá)式。

其中v是沒問題的,但100是字面量,在這綁定到const int&時(shí)必須實(shí)例化出一個(gè)int的臨時(shí)對(duì)象。正是這個(gè)臨時(shí)對(duì)象上發(fā)生了問題。

有人會(huì)說(shuō)這個(gè)臨時(shí)對(duì)象在max返回后失效了,但事實(shí)并非如此。

真相是,在一個(gè)完整的表達(dá)式里產(chǎn)生的臨時(shí)對(duì)象,它的生命周期從被創(chuàng)建完成開始,一直到完整的表達(dá)式結(jié)束時(shí)才結(jié)束。

也就是說(shuō)100這個(gè)臨時(shí)對(duì)象在max返回后其實(shí)還存在,但max的返回值不能延長(zhǎng)它的生命周期,value是通過引用進(jìn)行間接綁定的所以也不能延長(zhǎng)這個(gè)臨時(shí)對(duì)象的生命。最后完整的表達(dá)式結(jié)束,臨時(shí)對(duì)象100被消耗,現(xiàn)在value是懸垂引用了。

這就是典型的臨時(shí)對(duì)象導(dǎo)致的生命周期問題。

由于這個(gè)問題太常見,所以不僅是文檔和教程有列舉,比較新的編譯器也會(huì)有警告,比如GCC13。

除此之外就只能靠sanitizer來(lái)檢測(cè)了。sanitizer是一種編譯器在正常的生成代碼中插入一些特殊的監(jiān)測(cè)點(diǎn)來(lái)實(shí)現(xiàn)對(duì)程序行為監(jiān)控的技術(shù),比較常見的應(yīng)用是檢測(cè)有沒有不正常的內(nèi)存讀寫或者是多線程有沒有數(shù)據(jù)競(jìng)爭(zhēng)等問題。這里我們對(duì)懸垂引用的使用正好是一種不正常的內(nèi)存讀取,在檢測(cè)范圍內(nèi)。

編譯使用這個(gè)指令就能啟用檢測(cè):g++ -fsanitize=address xxx.cpp。遇到內(nèi)存相關(guān)的問題它會(huì)立刻報(bào)錯(cuò)并退出執(zhí)行。

問題的本質(zhì)在于max很容易產(chǎn)生臨時(shí)對(duì)象,但自己又完全沒法對(duì)這個(gè)臨時(shí)對(duì)象的生命周期產(chǎn)生影響,返回值不是引用可以一定程度上規(guī)避問題,然而作為通用的庫(kù)函數(shù),這里除了用引用又沒啥其他好辦法。所以這得算半個(gè)設(shè)計(jì)上的失誤。

不僅僅是max和min,所有參數(shù)是常量左值引用或者非轉(zhuǎn)發(fā)引用的右值引用,并且返回值的類型是引用且返回的是自己的某一個(gè)參數(shù)的函數(shù)都存在相同的問題。

想徹底解決問題有點(diǎn)難,但回避這個(gè)問題倒是不難:

// 方案1
const int maxValue = 100;
const int &value = std::max(v, maxValue);
// 方案2
const int value = std::max(v, 100);

方案1不需要產(chǎn)生臨時(shí)對(duì)象,value始終能引用到表達(dá)式結(jié)束后依然存在的變量。

方案2是比較推薦的,尤其是對(duì)標(biāo)量類型。由于臨時(shí)變量要在完整表達(dá)式結(jié)束后才銷毀,所以把它復(fù)制一份給value是完全沒問題的,賦值表達(dá)式也是完整表達(dá)式的一部分。這個(gè)方案的缺點(diǎn)在于復(fù)制成本較高或者無(wú)法復(fù)制的對(duì)象上不適用。但c++17把復(fù)制省略標(biāo)準(zhǔn)化了,這樣的表達(dá)式在大多數(shù)時(shí)候不會(huì)真的產(chǎn)生復(fù)制行為,所以我的建議是只要業(yè)務(wù)和語(yǔ)義上允許,優(yōu)先使用值語(yǔ)義也就是方案2,真出了問題并且定位到這里了再考慮轉(zhuǎn)換成方案1。

鏈?zhǔn)秸{(diào)用中的生命周期問題

從其他語(yǔ)言轉(zhuǎn)c++的人相當(dāng)容易踩這個(gè)坑??磦€(gè)最經(jīng)典的例子:

const char *str = path.trimmed().toStdString().c_str();

簡(jiǎn)單說(shuō)明下代碼,path是一個(gè)QString的實(shí)例,trimmed方法會(huì)返回一個(gè)去除了首尾全部空格的新的QString,toStdString()會(huì)復(fù)制底層數(shù)據(jù)然后轉(zhuǎn)換成一個(gè)std::string,c_str應(yīng)該不用我多說(shuō)了這個(gè)是把string內(nèi)部數(shù)據(jù)轉(zhuǎn)換成一個(gè)const char*的方法。

這句表達(dá)式同樣有問題,問題在于表達(dá)式結(jié)束后str會(huì)成為懸垂指針。

一步步來(lái)分解問題。首先c_str保證返回的指針有效,前提是調(diào)用c_str的那個(gè)string對(duì)象有效。如果string對(duì)象的生命周期結(jié)束了,那么c_str返回的指針也就無(wú)效了。

path.trimmed().toStdString()本身是沒問題的,每一步都是返回的新的值類型的對(duì)象實(shí)例,但是問題在于這些對(duì)象實(shí)例都是臨時(shí)對(duì)象,但我們沒有做任何措施來(lái)延長(zhǎng)臨時(shí)對(duì)象的生命周期,整句表達(dá)式結(jié)束后它們就全析構(gòu)生命周期終結(jié)了。

現(xiàn)在問題應(yīng)該明了了,臨時(shí)對(duì)象上調(diào)了c_str,但這個(gè)臨時(shí)對(duì)象表達(dá)式結(jié)束后不存在了。所以str最后變成了懸垂指針。

為啥會(huì)坑到其他語(yǔ)言轉(zhuǎn)來(lái)的人呢?因?yàn)閷?duì)于有g(shù)c的語(yǔ)言,上述表達(dá)式實(shí)際上又產(chǎn)生了新的到臨時(shí)對(duì)象的可達(dá)路徑,所以對(duì)象是不會(huì)回收的,而對(duì)于rust之類的語(yǔ)言還可以精細(xì)控制讓對(duì)象的每一部分具有不同的生命周期,上述表達(dá)式稍微改改是有機(jī)會(huì)正常使用的。這些語(yǔ)言轉(zhuǎn)到c++把老習(xí)慣帶過來(lái)就要被坑了。

推薦的解決辦法只有1種:

auto tmp = path.trimmed().toStdString();
const char *str = tmp.c_str();

能解決問題,但毛病也很明顯,需要多個(gè)用完就扔的變量出來(lái),而且這個(gè)變量因?yàn)楦鶕?jù)后續(xù)的操作要求很可能還不能用const修飾,這東西不僅干擾思維,有時(shí)候還會(huì)成為定時(shí)炸彈。

我不推薦直接用string而不用指針,是因?yàn)橛袝r(shí)候不得不用const char*,這種時(shí)候啥方法都不好使,只能用上面的辦法去暫存臨時(shí)數(shù)據(jù),以便讓它的生命周期能延長(zhǎng)到后續(xù)操作結(jié)束為止。

三元運(yùn)算符中的生命周期問題

三元運(yùn)算符中也有類似的問題。我們看個(gè)例子:

const std::string str = func();
std::string_view pretty = str.empty() ? "<empty>" : str;

很簡(jiǎn)單的一行代碼,我們判斷字符串是不是空的,如果是就轉(zhuǎn)換成特殊的占位符字符串。用string_view當(dāng)然是因?yàn)槲覀儾幌霃?fù)制出一份str,所以只用string_view來(lái)引用原來(lái)的字符串,而且string_view也能引用字符串字面量,用在這里看起來(lái)正合適。

事實(shí)是這段代碼無(wú)比的危險(xiǎn)。而且-Wall-Wextra都沒法讓編譯器在編譯時(shí)檢測(cè)到問題,我們得用sanitizer:g++ -std=c++20 -Wall -Wextra -fsanitize=address test.cpp。接著運(yùn)行程序,我們會(huì)看到這樣的報(bào)錯(cuò):ERROR: AddressSanitizer: stack-use-after-scope on address ...

這個(gè)報(bào)錯(cuò)提示我們使用了某個(gè)已經(jīng)析構(gòu)了的變量。而且新版本的編譯器還會(huì)很貼心得告訴你就是使用了pretty這個(gè)變量導(dǎo)致的。

不過雖然我們知道了具體是哪一行的那個(gè)變量導(dǎo)致的問題,但原因卻不知道,而且當(dāng)我們的字符串不為空的時(shí)候也不會(huì)觸發(fā)問題。

這個(gè)時(shí)候其實(shí)就是語(yǔ)法規(guī)則在作祟了。

c++里規(guī)定三元運(yùn)算符產(chǎn)生的結(jié)果最終只能有一種統(tǒng)一的類型。這個(gè)好理解,畢竟要賦值給某個(gè)固定類型的變量的表達(dá)式產(chǎn)生大于一種可能的結(jié)果類型既不合邏輯也很難正確編譯。

但這導(dǎo)致了一個(gè)問題,如果三元運(yùn)算符兩邊的表達(dá)式確實(shí)有不同的結(jié)果類型怎么辦?現(xiàn)代語(yǔ)言通常的做法是直接報(bào)錯(cuò),然而c++的做法是按照語(yǔ)法規(guī)則做類型轉(zhuǎn)換,實(shí)在轉(zhuǎn)換不來(lái)才會(huì)報(bào)錯(cuò)??雌饋?lái)c++的做法更寬松,這反過來(lái)誘發(fā)了這節(jié)所述的問題。

我們看看具體的轉(zhuǎn)換規(guī)則:

  • 兩個(gè)表達(dá)式有一邊產(chǎn)生void值另一邊不是,那么三元運(yùn)算符結(jié)果的類型和另一個(gè)不是結(jié)果不是void的表達(dá)式的相同(產(chǎn)生void的表達(dá)式只能是throw表達(dá)式,否則算語(yǔ)法錯(cuò)誤)
  • 兩個(gè)表達(dá)式都產(chǎn)生void,則結(jié)果也是void,這里不要求只能是throw表達(dá)式
  • 兩個(gè)表達(dá)式結(jié)果類型相同,那么三元運(yùn)算符的結(jié)果類型和表達(dá)式相同
  • 兩個(gè)表達(dá)式結(jié)果類型不同或者具有不同的cv限定符,那么得看是否有其中一個(gè)類型能隱式轉(zhuǎn)換成另一個(gè),如果沒有那么是語(yǔ)法錯(cuò)誤,如果兩方能互相轉(zhuǎn)換,也是語(yǔ)法錯(cuò)誤。滿足這個(gè)限定條件,那么另一個(gè)類型的表達(dá)式的結(jié)果會(huì)被隱式類型轉(zhuǎn)換成目標(biāo)類型,比如當(dāng)出現(xiàn)const char *std::string的時(shí)候,因?yàn)榇嬖?code>const char *隱式轉(zhuǎn)換成string的方法,所以最終三元運(yùn)算符的結(jié)果類型是std::string;而Tconst T通常結(jié)果類型是const T。

這還是我掐頭去尾簡(jiǎn)化了好幾次的總結(jié)版,實(shí)際的規(guī)則更復(fù)雜,如果我把實(shí)際上的規(guī)則列在那難免被噴是語(yǔ)言律師,所以我就不自討沒趣了。但這個(gè)簡(jiǎn)化版規(guī)則雖然粗糙,但實(shí)際開發(fā)倒是基本夠用了。

回到我們出問題的表達(dá)式,因?yàn)閜retty初始化后就沒再修改過,那100%就是三元運(yùn)算符那里有什么貓膩。恰巧的是我們正好對(duì)應(yīng)在第四點(diǎn)上,表達(dá)式類型不同但可以進(jìn)行隱式轉(zhuǎn)換。

按照規(guī)則,字符串字面量"<empty>"要轉(zhuǎn)換成const std::string,正好存在這樣的隱式轉(zhuǎn)換序列(const char[8] -> const char * -> std::string, 隱式轉(zhuǎn)換序列怎么得出的可以看這里),當(dāng)表達(dá)式為真也就是我們的字符串是空的,一個(gè)臨時(shí)的string對(duì)象就被構(gòu)造出來(lái)了。接著會(huì)從這個(gè)臨時(shí)的string構(gòu)造一個(gè)string_view,string_view只是簡(jiǎn)單地和原來(lái)的string共有內(nèi)部數(shù)據(jù),本身沒有str的所有權(quán),而且string_view也不是“引用”,所以它不能延長(zhǎng)臨時(shí)對(duì)象的生命周期。接著完整的表達(dá)式結(jié)束了,這時(shí)在表達(dá)式內(nèi)創(chuàng)建的臨時(shí)對(duì)象如果沒有什么能延長(zhǎng)它生命的東西存在,就會(huì)被析構(gòu)。顯然在這一步從"<empty>"轉(zhuǎn)換來(lái)的臨時(shí)string就析構(gòu)了。

現(xiàn)在我們發(fā)現(xiàn)和pretty共有數(shù)據(jù)的string被銷毀了,后面繼續(xù)用pretty顯然是錯(cuò)誤的。

從別的語(yǔ)言轉(zhuǎn)c++的開發(fā)者估計(jì)很容易踩到這種坑,短的字符串字面量轉(zhuǎn)換成string在libstdc++還有特殊優(yōu)化,在這個(gè)優(yōu)化下你的程序就算犯了上述錯(cuò)誤10次里還是有七八次能正常運(yùn)行,然后剩下兩三次得到錯(cuò)誤或者崩潰;要是換了另一個(gè)不同的標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)那就有更多的未知在等著你了。這也是string_view在標(biāo)準(zhǔn)中標(biāo)明的幾個(gè)undefined behavior之一。所以這個(gè)錯(cuò)誤經(jīng)驗(yàn)不足的話會(huì)非常隱蔽。

修復(fù)倒是不難,如果能變更pretty的類型(后續(xù)可以從pretty創(chuàng)建string_view),那有下面幾種方案可選:

// 方案1
std::string_view pretty = str;
if (str.empty()) {
    pretty = "<empty>";
}
// 方案2
const std::string pretty = str.empty() ? "<empty>" : str;
// 方案3
const std::string &pretty = str.empty() ? "<empty>" : str;

方案1里不再有類型轉(zhuǎn)換和臨時(shí)對(duì)象了,字符串字面量的生命周期從程序運(yùn)行開始到程序退出結(jié)束,沒有生命周期問題。但這個(gè)方案會(huì)顯得比較啰嗦而且在字符串為空的時(shí)候得多一次賦值。

方案2也沒啥特別要說(shuō)的,就是前幾節(jié)講的在臨時(shí)對(duì)象銷毀前復(fù)制了一份。對(duì)于標(biāo)量類型這么做一般沒問題,對(duì)于類類型就得考慮復(fù)制成本了,不過編譯器通常能做到copy elision,倒不用特別擔(dān)心。

方案3其實(shí)也比較容易理解,我們不是產(chǎn)生了臨時(shí)對(duì)象么,那么直接用常量左值引用去綁定,這樣臨時(shí)對(duì)象的生命周期就能被擴(kuò)展延長(zhǎng)了,而且const T &本來(lái)就能綁定到str這樣的左值上,所以語(yǔ)法上沒問題運(yùn)行時(shí)也沒有問題。

特例

說(shuō)完三個(gè)典型問題,還有兩個(gè)特例。

第一個(gè)是關(guān)于引用臨時(shí)對(duì)象的非static數(shù)據(jù)成員的。具體例子如下:

具體的例子如下:

struct Data {
    int a;
    std::string b;
    bool c;
};
Data get_data(int a, const std::string &b, bool c)
{
    return {a, b, c};
}
int main()
{
    std::cout << get_data(1, "test", false).b << '\n';
    const auto &str = get_data(1, "test", false).b;
    std::cout << str << '\n';
}

這個(gè)例子是沒有問題的。原因在于,如果我們用引用綁定了臨時(shí)對(duì)象的非static數(shù)據(jù)成員,也就是subobject,那么不僅僅是數(shù)據(jù)成員,整個(gè)臨時(shí)對(duì)象的生命周期都會(huì)得到延長(zhǎng)。所以這里str雖然只綁定到了成員b,但整個(gè)臨時(shí)對(duì)象會(huì)獲得和str一樣的生命周期,所以不會(huì)在完整的表達(dá)式結(jié)束后銷毀,因此后續(xù)繼續(xù)使用str是安全的。

這個(gè)subobject還包括數(shù)組元素,所以const int &num = <temp-array>[index];也會(huì)導(dǎo)致整個(gè)數(shù)組的生命周期被延長(zhǎng)。

符合要求的形式還有很多,這里就不一一列舉了。

不過這個(gè)特例帶來(lái)了風(fēng)險(xiǎn),因?yàn)橥暾磉_(dá)式結(jié)束后我們?cè)L問不到其他成員了,但它們都還實(shí)際存在,這會(huì)留下資源泄露的隱患?,F(xiàn)代的編程語(yǔ)言也基本都是這么做的,為了照顧大部分人的習(xí)慣倒也無(wú)可厚非,自己注意一下就行。

第二個(gè)特例是for-range循環(huán)。先看例子:

class Data {
    std::vector<int> data_;
public:
    Data(std::initializer_list<int> l): data_(l)
    {}
    const std::vector<int> &get_data() const
    {
        return data_;
    }
};
int main()
{
    for (const auto &v: Data{1, 2, 3, 4, 5}.get_data()) {
        std::cout << v << '\n';
    }
}

在c++23之前,這是錯(cuò)的,實(shí)際上我們用msvc運(yùn)行會(huì)看到什么也沒輸出,用GCC和sanitize則直接報(bào)錯(cuò)了。GCC同時(shí)還會(huì)直接給出警告告訴你這里有懸垂引用。

問題倒是不難理解,for循環(huán)里冒號(hào)右側(cè)的表達(dá)式實(shí)際上是一個(gè)完整的表達(dá)式,并且在進(jìn)入for循環(huán)之前就計(jì)算完了,所以臨時(shí)對(duì)象被銷毀,我們通過引用返回值間接傳遞出來(lái)的東西自然也就失效了。

然而這是語(yǔ)言設(shè)計(jì)上的bug。同樣作為初始化語(yǔ)句,for (int i=xxx, i < xx, ++i)中的i的生命周期就是從初始化開始,到for循環(huán)結(jié)束才結(jié)束的,所以形式上類似的for-range沒有理由作為例外,否則很容易產(chǎn)生陷阱并限制使用上的便利性。

如果只是和普通for循環(huán)有差異那倒還好,問題是標(biāo)準(zhǔn)規(guī)定了for-range需要轉(zhuǎn)換成某些規(guī)定形式,這會(huì)導(dǎo)致下面的結(jié)果:

// 正常的沒有問題
for (const auto &v : std::vector{1,2,3,4,5}) {
    std::cout << v << '\n';
}

同樣都是初始化語(yǔ)句里的臨時(shí)變量,怎么一個(gè)有生命周期問題一個(gè)沒有?因?yàn)楹蜆?biāo)準(zhǔn)規(guī)定的轉(zhuǎn)換形式有關(guān),感興趣的可以去深究一下。但這是實(shí)打?qū)嵉男袨槊?,就像一個(gè)人早上說(shuō)自己是地球人但吃完午飯就改口說(shuō)自己是大猩猩一樣荒謬。

這個(gè)bug也有一段時(shí)間了,直到前年才有提案來(lái)想辦法解決,不過好消息是已經(jīng)被接受進(jìn)c++23了,現(xiàn)在for-range的初始化語(yǔ)句中產(chǎn)生的臨時(shí)對(duì)象的生命周期會(huì)延長(zhǎng)到for-range循環(huán)結(jié)束,不管是什么形式的。

可惜到目前為止,我還沒看到有編譯器支持(GCC 14.1,clang 18.1.8),作為臨時(shí)解決辦法,你只能這么寫:

int main()
{
    const auto &tmp = Data{1, 2, 3, 4, 5};
    for (const auto &v: tmp.get_data()) {
        std::cout << v << '\n';
    }
}

如何發(fā)現(xiàn)生命周期問題

既然這些坑這么危險(xiǎn)又這么隱蔽,那有辦法及時(shí)發(fā)現(xiàn)防患于未然嗎?

這還是比較難的,也是當(dāng)今的熱門研究方向。

rust選擇了用類型系統(tǒng)+編譯檢測(cè)來(lái)扼殺生命周期問題,但效果不太理想,除了issue里那些bug之外,緩慢的編譯速度和無(wú)法簡(jiǎn)單實(shí)現(xiàn)某些數(shù)據(jù)結(jié)構(gòu)也是不小的問題。但整體來(lái)說(shuō)還是比c++前進(jìn)了很多步,上面列舉的三類問題一些是語(yǔ)法規(guī)則禁止的,另一些則能在編譯時(shí)檢測(cè)出來(lái)。

c++語(yǔ)法已經(jīng)成型也很難引進(jìn)太大的變化,想及時(shí)發(fā)現(xiàn)問題,就得依賴這三樣了:

  • constexpr
  • sanitizer
  • 靜態(tài)分析

constexpr里禁止任何形式的內(nèi)存泄露,也禁止越界訪問和使用已經(jīng)析構(gòu)的數(shù)據(jù),但這些檢測(cè)只有在編譯期計(jì)算時(shí)才進(jìn)行,而且不是什么東西都能放進(jìn)constexpr的,所以雖然能發(fā)現(xiàn)生命周期問題,但限制太大。

sanitizer沒有constexpr那么多限制,而且檢測(cè)的種類更多也更仔細(xì),但缺點(diǎn)是需要程序真正運(yùn)行到有問題的代碼上才能上報(bào),如果不想每次都運(yùn)行整個(gè)程序你就得有一個(gè)質(zhì)量上乘的單元測(cè)試集;sanitizer還會(huì)拖慢性能,以address檢測(cè)器為例,平均而言會(huì)導(dǎo)致性能下降1到2倍,盡管已經(jīng)比valgrind這樣的工具快多了,但有時(shí)候還是會(huì)因?yàn)樘鴰?lái)不便。

靜態(tài)分析不需要運(yùn)行實(shí)際代碼,它會(huì)分析代碼的調(diào)用路徑和操作,然后根據(jù)一定的模式來(lái)找出看起來(lái)有問題的代碼。好處是不用實(shí)際運(yùn)行,安裝配置簡(jiǎn)單,編譯器一般還自帶了一個(gè)可以用;壞處是容易誤報(bào),分析能力有時(shí)不如人類尤其是邏輯比較復(fù)雜時(shí)。

工具各有千秋,結(jié)合起來(lái)一起使用是比較常見的工程實(shí)踐。

個(gè)人的知識(shí)和經(jīng)驗(yàn)也絕不能落下,因?yàn)閺木幋a這個(gè)源頭上就扼殺生命周期問題是目前最經(jīng)濟(jì)有效的辦法。

總結(jié)

常見的表達(dá)式中臨時(shí)變量導(dǎo)致的生命周期問題就是這些了。

modern c++其實(shí)一直在推行值語(yǔ)義,一定程度上可以緩解這些問題,但c++真的太復(fù)雜了,永遠(yuǎn)沒有銀彈能解決所有問題。還是得自己慢慢積累知識(shí)和經(jīng)驗(yàn)才行。

參考資料

https://en.cppreference.com/w/cpp/language/operator_other

到此這篇關(guān)于c++臨時(shí)對(duì)象導(dǎo)致的生命周期問題 的文章就介紹到這了,更多相關(guān)c++臨時(shí)對(duì)象內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 解析wprintf 中使用%I64d格式化輸出LONGLONG的詳細(xì)介紹

    解析wprintf 中使用%I64d格式化輸出LONGLONG的詳細(xì)介紹

    本篇文章是對(duì)wprintf 中使用%I64d格式化輸出LONGLONG進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-05-05
  • C++?QT?QThread啟動(dòng)、停止、暫停和恢復(fù)的實(shí)現(xiàn)

    C++?QT?QThread啟動(dòng)、停止、暫停和恢復(fù)的實(shí)現(xiàn)

    本文主要介紹了C++?QT?QThread啟動(dòng)、停止、暫停和恢復(fù)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • MySQL系列教程之使用C語(yǔ)言來(lái)連接數(shù)據(jù)庫(kù)

    MySQL系列教程之使用C語(yǔ)言來(lái)連接數(shù)據(jù)庫(kù)

    c語(yǔ)言操作Mysql數(shù)據(jù)庫(kù),主要就是為了實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的增、刪、改、查等操作,下面這篇文章主要給大家介紹了關(guān)于MySQL系列教程之使用C語(yǔ)言來(lái)連接數(shù)據(jù)庫(kù)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-09-09
  • 詳解C語(yǔ)言和Python中的線程混用

    詳解C語(yǔ)言和Python中的線程混用

    這篇文章主要介紹了C和Python中的線程混用的相關(guān)資料,文中講解非常細(xì)致,幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-07-07
  • 詳解C++的靜態(tài)內(nèi)存分配與動(dòng)態(tài)內(nèi)存分配

    詳解C++的靜態(tài)內(nèi)存分配與動(dòng)態(tài)內(nèi)存分配

    內(nèi)存分配 (Memory Allocation) 是指為計(jì)算機(jī)程序或服務(wù)分配物理內(nèi)存空間或虛擬內(nèi)存空間的一個(gè)過程,本文主要介紹了C++的靜態(tài)內(nèi)存分配與動(dòng)態(tài)內(nèi)存分配,感興趣的同學(xué)可以參考閱讀
    2023-06-06
  • C語(yǔ)言關(guān)鍵字auto與register的深入理解

    C語(yǔ)言關(guān)鍵字auto與register的深入理解

    本篇文章是對(duì)c語(yǔ)言關(guān)鍵字auto與register的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-05-05
  • C語(yǔ)言實(shí)現(xiàn)的順序表功能完整實(shí)例

    C語(yǔ)言實(shí)現(xiàn)的順序表功能完整實(shí)例

    這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)的順序表功能,結(jié)合完整實(shí)例形式分析了C語(yǔ)言順序表的創(chuàng)建、添加、刪除、排序、合并等相關(guān)操作技巧,需要的朋友可以參考下
    2018-04-04
  • C++實(shí)現(xiàn)二維圖形的傅里葉變換

    C++實(shí)現(xiàn)二維圖形的傅里葉變換

    這篇文章主要介紹了C++實(shí)現(xiàn)二維圖形的傅里葉變換的方法,是C++程序設(shè)計(jì)里一個(gè)重要的應(yīng)用,需要的朋友可以參考下
    2014-08-08
  • Libevent的使用及reactor模型詳解

    Libevent的使用及reactor模型詳解

    Libevent?是一個(gè)用C語(yǔ)言編寫的、輕量級(jí)的開源高性能事件通知庫(kù),主要有以下幾個(gè)亮點(diǎn):事件驅(qū)動(dòng)(?event-driven),高性能;輕量級(jí),專注于網(wǎng)絡(luò),這篇文章主要介紹了Libevent的使用及reactor模型,需要的朋友可以參考下
    2024-03-03
  • C++中Lambda表達(dá)式的語(yǔ)法與實(shí)例

    C++中Lambda表達(dá)式的語(yǔ)法與實(shí)例

    C++ 11 中的 Lambda 表達(dá)式用于定義并創(chuàng)建匿名的函數(shù)對(duì)象,以簡(jiǎn)化編程工作,下面這篇文章主要給大家介紹了關(guān)于C++中Lambda表達(dá)式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2021-10-10

最新評(píng)論