新聞中心
作者 | Jimmy Hartzell

10年積累的成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)經(jīng)驗(yàn),可以快速應(yīng)對(duì)客戶對(duì)網(wǎng)站的新想法和需求。提供各種問題對(duì)應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識(shí)你,你也不認(rèn)識(shí)我。但先做網(wǎng)站后付款的網(wǎng)站建設(shè)流程,更有宣威免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
策劃 | 云昭
本文的作者Jimmy Hartzell是一名在公司內(nèi)部教授高級(jí)C++課程的專家,卻在重返“現(xiàn)代化”C++之后,對(duì)這門語言的改進(jìn)感到非常失望。在這篇文章中,作者將重點(diǎn)討論各種C++的小“毒瘤”,這些“毒瘤”的設(shè)計(jì)決策讓作者大有“恨鐵不成鋼”之感。
作者同時(shí)也有豐富的Rust經(jīng)驗(yàn),但并沒有將C++與 Rust 或其他編程語言進(jìn)行比較,而是重點(diǎn)從 C++ 的角度來探討這些看似很有技術(shù)含量的“毒瘤”究竟有沒有意義。
一、“技巧”并不高大上,也不值得炫耀
重返C++開發(fā),我自信滿滿,滿懷期待:我仍然懷揣C++各種“吊詭”技能,并且仍然可以高效地工作,而且如今我已經(jīng)使用了一種更現(xiàn)代的編程語言,遺留問題應(yīng)該會(huì)更少。但現(xiàn)實(shí)是C++ 帶來的刺痛更多。
Rust 中有很多我懷念的功能,都能在C++中輕松添加,比如很明顯的安全功能,再比如對(duì)sum type(在 Rust 中稱為 enums)或元組(tuple)的first-class支持。(澄清:std::tuple和std::variant 不是first-class的支持,這兩個(gè)實(shí)際上非常的笨重。)
不過,在開始討論吊詭的“剪紙”技巧之前,我想先談?wù)勎宜吹降?C++ 的主要防御“措辭”之一,我發(fā)現(xiàn)下面的描述特別令人困惑:
C++ 是一種很棒的編程語言。這些抱怨只是來自那些不勝任的人。如果他們是更好的程序員,他們會(huì)欣賞 C++ 的做事方式,并且他們不需要他們的幫助。像 Rust 這樣的語言對(duì)于真正的專業(yè)人士來說沒有幫助。
顯然,這種措辭有點(diǎn)“抓馬”,但我已經(jīng)多次看到這種態(tài)度。我對(duì)它最寬容的看法是,C++ 的困難正是其功能強(qiáng)大的標(biāo)志,也是使用強(qiáng)大的編程語言的自然成本。然而,在很多情況下,它對(duì)我來說是精英主義的一種形式:讓“弱雞”程序員變得輕松是一個(gè)毫無意義的大眾想法,而優(yōu)秀的程序員不會(huì)從讓事情變得更容易中受益。
作為一個(gè)在我職業(yè)生涯的大部分時(shí)間里都從事 C++ 專業(yè)編程并且教授(公司內(nèi)部)高級(jí) C++ 課程的人,這對(duì)我來說簡直是無稽之談。我確實(shí)知道如何駕馭 C++ 的許多“剪紙和避雷”的技巧,并且很高興在處理 C++ 代碼庫時(shí)這樣做。但盡管我經(jīng)驗(yàn)豐富,但它們?nèi)匀粫?huì)拖慢我的速度并分散我的注意力,將注意力從我試圖解決的實(shí)際問題上轉(zhuǎn)移開,并導(dǎo)致代碼的可維護(hù)性較差。
至于好處,我沒看到C++的這些技巧有什么高大上的。除非是遺留代碼庫或者僅在恰好不支持 Rust 的特定編譯器中可用的優(yōu)化方面,C++ 比 Rust 更高效或更合適,否則這些技巧大多用來處理跟編程語言的實(shí)際設(shè)計(jì)無關(guān)的其他問題。
雖然我為自己的 C++ 技能感到自豪,但令我感到擔(dān)憂的是,這些看起來“更好的技術(shù)”可以使它們部分過時(shí)。即便擁有了讓它變得更容易的功能,我也不會(huì)欣賞。在大多數(shù)情況下,這種“技巧”并不能使C++為我解決更多工作的問題,反而是C++ 創(chuàng)造了不必要的額外工作的問題,因?yàn)槭褂眠@些所謂的技巧本身就讓你忽略了你工作的目的——不要這樣做。不要讓我開始研究頭文件!
二、雞肋的“剪紙”技巧,意義不大
我也希望我的編程語言對(duì)初學(xué)者友好。我總是會(huì)與其他具有各種技能的程序員一起工作,而且我寧愿不必糾正我同事的錯(cuò)誤——或者我自己早期、更愚蠢版本的錯(cuò)誤。雖然我也不同意為了讓一種編程語言對(duì)初學(xué)者更友好而犧牲掉功能,但許多甚至大多數(shù) C++ 對(duì)初學(xué)者不友好(并且令專家厭煩)的功能,實(shí)際上并沒有使該語言變得更強(qiáng)大。
言歸正傳,以下是我在從Rust回歸 C++ 開發(fā)時(shí)注意到的最大的雞肋“剪紙”技巧。
1.const不是默認(rèn)值
const當(dāng)可以標(biāo)記參數(shù)時(shí),很容易忘記標(biāo)記參數(shù)。你可能只是忘記輸入關(guān)鍵字。對(duì)于 來說尤其如此 this,它是一個(gè)隱式參數(shù):你沒有時(shí)間顯式地輸入this參數(shù),因此如果沒有適當(dāng)?shù)男揎椃?,它不?huì)坐在那里看起來很有趣。
如果 C++ 有相反的默認(rèn)值,其中除非顯式聲明每個(gè)值、引用和指針都是const可變的,那么我們更有可能根據(jù)函數(shù)是否需要改變它來正確聲明每個(gè)參數(shù)。如果有人包含mutable關(guān)鍵字,那是因?yàn)樗麄冎雷约盒枰H绻麄冃枰浟?,編譯器錯(cuò)誤會(huì)提醒他們。
現(xiàn)在,你可能認(rèn)為這并不重要,因?yàn)槟憧梢圆皇褂胏onst或不擁有具有不需要的功能的函數(shù) - 但有時(shí)你必須const在 C++ 中接受這些事情。如果你通過非引用獲取參數(shù)const,則調(diào)用者只能使用左值來調(diào)用你的函數(shù)。但如果通過引用獲取參數(shù)const,調(diào)用者可以使用左值或右值。因此,為了以自然的方式使用某些函數(shù),必須通過const引用獲取其參數(shù)。
一旦有了const引用,你就只能(輕松)用它調(diào)用接受const引用的函數(shù),因此,如果其中任何函數(shù)忘記聲明參數(shù)const,則必須包含const_cast – 或稍后更改函數(shù)以正確接受const。
以免你認(rèn)為這只是一個(gè)草率的新手錯(cuò)誤,請注意,標(biāo)準(zhǔn)庫中的許多函數(shù)必須更新,以代替 const_iterator或補(bǔ)充,iterator當(dāng)正確發(fā)現(xiàn)它們對(duì)像const_iterator: 這樣的函數(shù)有意義時(shí)erase。事實(shí)證明,對(duì)于像 之類的函數(shù)erase,集合必須是可變的,而不是迭代器——C++ 庫的維護(hù)者一開始就犯了錯(cuò)誤。
2.強(qiáng)制Copy
在 C++ 中,可復(fù)制對(duì)象是對(duì)象行為的默認(rèn)特權(quán)方式。如果你不希望對(duì)象可復(fù)制,并且其所有字段都可復(fù)制,則通常必須將復(fù)制構(gòu)造函數(shù)和復(fù)制賦值運(yùn)算符標(biāo)記為= delete。默認(rèn)情況下,編譯器會(huì)為你編寫代碼 - 代碼可能不正確。
但是,如果你確實(shí)讓你的class只能移動(dòng),請小心,因?yàn)檫@意味著在某些情況下你無法使用它。在 C++11 中,沒有符合人體工程學(xué)的方法來通過 move 進(jìn)行 lambda 捕獲——這通常是我想要將變量捕獲到閉包中的方式。
這在 C++14 中已被“修復(fù)”——因?yàn)楫?dāng)你想要從一開始就應(yīng)該使用默認(rèn)值時(shí),你現(xiàn)在可以使用極其笨拙的移動(dòng)捕獲語法。
然而,即便如此,祝你使用 lambda 好運(yùn)。如果你想把它放在 a 中std::function,那么直到今天你仍然不走運(yùn)。std::function期望它管理的對(duì)象是可復(fù)制的,如果你的閉包對(duì)象是僅移動(dòng)的,則將無法編譯。
這個(gè)問題將在 C++23 中得到解決, std::move_only_function 但與此同時(shí),我被迫使用拋出某種運(yùn)行時(shí)邏輯異常的復(fù)制構(gòu)造函數(shù)來編寫類。即使在 C++23 中,可復(fù)制函數(shù)也將是默認(rèn)的假設(shè)情況。
奇怪的是,因?yàn)榇蠖鄶?shù)復(fù)雜的對(duì)象,尤其是閉包,永遠(yuǎn)不會(huì)也不應(yīng)該被復(fù)制。一般來說,復(fù)制復(fù)雜的數(shù)據(jù)結(jié)構(gòu)是一個(gè)錯(cuò)誤——缺少&或缺少std::move。但這是一個(gè)錯(cuò)誤,沒有任何警告,并且代碼中沒有明顯的跡象表明正在執(zhí)行復(fù)雜的、需要大量分配的操作。這是給新 C++ 開發(fā)人員的早期教訓(xùn)——不要按值傳遞非原始類型——但即使是高級(jí)開發(fā)人員也可能時(shí)不時(shí)地搞砸,而且一旦它進(jìn)入代碼庫,就很容易錯(cuò)過。
3.通過引用獲取參數(shù):反人性的設(shè)計(jì)
在 C++ 中按元組返回多個(gè)值是反人性的。std::tie這是可以做到的,但是對(duì)和 的調(diào)用std::make_tuple是冗長且分散注意力的,更不用說你將不習(xí)慣地編寫,這對(duì)于正在閱讀和調(diào)試你的代碼的人來說總是不好的。
旁注:有人在評(píng)論中提出了結(jié)構(gòu)化綁定,好像這解決了問題。結(jié)構(gòu)化綁定是現(xiàn)代 C++ 支持者喜歡引用的半途而廢的一個(gè)很好的例子。結(jié)構(gòu)化綁定對(duì)某些人有幫助,但如果你認(rèn)為它們使按元組返回符合人體工程學(xué),那你就錯(cuò)了。你仍然需要在函數(shù)返回語句中或 在函數(shù)的返回類型中寫入std::pair或 。這不是最糟糕的,但它仍然不如完整的一流元組支持那么輕量級(jí),并且還不足以說服人們不使用參數(shù),這是我真正的抱怨。std::make_tuplestd::tuple即便如此,并不是輸出參數(shù)(或輸入輸出參數(shù))不好,而是它們在 C++ 中不好,因?yàn)闆]有好的方法來表達(dá)它們。
那么我們該怎么辦呢?元組的笨重導(dǎo)致人們轉(zhuǎn)而使用參數(shù)。要使用輸出參數(shù),你最終會(huì)通過非引用獲取參數(shù)const,這意味著該函數(shù)應(yīng)該修改該參數(shù)。
問題是,這僅在函數(shù)簽名中標(biāo)記。如果你有一個(gè)通過引用獲取參數(shù)的函數(shù),則該參數(shù)在調(diào)用站點(diǎn)看起來與按值參數(shù)相同:
// Return false on failure. Modify size with actual message size,
// decreasing it if it contains more than one message.
bool got_message(const char *void mesg, size_t &size);
size_t size = buff.size();
got_message(buff.data(), size);
buff.resize(size);如果你快速閱讀調(diào)用代碼,則該調(diào)用可能看起來 resize是多余的,但事實(shí)并非如此。size正在被 修改 got_message,并且知道它正在被修改的唯一方法是查看函數(shù)簽名,該函數(shù)簽名通常位于另一個(gè)文件中。
出于這個(gè)原因,有些人更喜歡通過指針傳遞 out 參數(shù)和 in-out 參數(shù):
bool got_message(const char *void mesg, size_t *size);
size_t size = buff.size();
got_message(buff.data(), &size);
buff.resize(size);如果指針不可為空的話,那就太好了。nullptr在這種情況下參數(shù)意味著什么?它會(huì)觸發(fā)未定義的行為嗎?如果將調(diào)用者的指針傳遞給它會(huì)怎樣?開發(fā)者經(jīng)常忘記記錄函數(shù)如何使用空指針。
這可以通過不可為空的指針來解決,但很少有程序員在實(shí)踐中真正這樣做。當(dāng)某些東西不是默認(rèn)值時(shí),它往往不會(huì)在適當(dāng)?shù)牡胤绞褂?。?duì)此的可持續(xù)答案是改變默認(rèn)設(shè)置,而不是與人性作斗爭的英勇嘗試。
4.方法實(shí)現(xiàn)可能會(huì)矛盾
在 C++ 中,每次編寫一個(gè)類(尤其是較低級(jí)別的類)時(shí),你都有責(zé)任對(duì)編程語言中具有特殊語義重要性的某些方法做出決策:
- 構(gòu)造函數(shù)(copy):X(const X&)
- 構(gòu)造函數(shù)(move):X(X&&)
- 作業(yè)(copy):operator=(const X&)
- 作業(yè)(move):operator=(X&&)
- 析構(gòu)函數(shù):~X()
對(duì)于許多類,默認(rèn)實(shí)現(xiàn)就足夠了,如果可能的話你應(yīng)該依賴它們。這是否可行取決于簡單地復(fù)制所有字段是否是復(fù)制整個(gè)對(duì)象的明智方法,而這很容易忘記考慮。
但是,如果你需要其中之一的自定義實(shí)現(xiàn),那么你就需要編寫所有這些。這就是所謂的“5 法則”。你必須編寫所有這些,即使兩個(gè)賦值運(yùn)算符的正確行為可以完全由適當(dāng)?shù)臉?gòu)造函數(shù)與析構(gòu)函數(shù)相結(jié)合來確定。編譯器可以默認(rèn)實(shí)現(xiàn)引用這些其他函數(shù)的賦值運(yùn)算符,因此始終是正確的,但事實(shí)并非如此。正確實(shí)現(xiàn)它們是很棘手的,需要諸如顯式防止自分配或與按值參數(shù)交換等技術(shù)。無論如何,它們都是樣板文件,并且是具有許多此類內(nèi)容的編程語言中可能出錯(cuò)的另一件事。
旁注:確實(shí),許多類可以使用= default所有這些方法。但是,如果你自定義復(fù)制構(gòu)造函數(shù)或移動(dòng)構(gòu)造函數(shù),則還必須自定義賦值運(yùn)算符以匹配,即使默認(rèn)實(shí)現(xiàn)可能是正確的(如果語言定義得更智能的話)。通過引用5規(guī)則就可以清楚地看出這一點(diǎn),它基本上說明了這一點(diǎn)。完整的規(guī)則在CPP參考中進(jìn)行了解釋。如果自定義復(fù)制或移動(dòng)構(gòu)造函數(shù),相應(yīng)的= default 賦值運(yùn)算符將會(huì)出錯(cuò)。當(dāng)心!請注意示例代碼如何不使用= default賦值運(yùn)算符,即使賦值運(yùn)算符不包含邏輯。
三、C++ 做了太多錯(cuò)誤的決策
看到 Hacker News 上的評(píng)論后,我覺得有必要添加這一部分。每當(dāng)有人抱怨 C++ 中的任何問題時(shí),就會(huì)有人提到修復(fù)該問題的較新版本的 C++。這些“修復(fù)”通常不是那么好,只有當(dāng)你習(xí)慣了一切都有點(diǎn)笨拙時(shí),才感覺像是修復(fù)。
原因如下:
- 默認(rèn)方式仍然是舊的、糟糕的方式。例如,通過 move 捕獲 lambda 應(yīng)該是默認(rèn)值,而std::move_only_functionC++23 中即將推出的 lambda 應(yīng)該是默認(rèn)值std::function。
- 出于這個(gè)原因,并且因?yàn)榕f的、糟糕的方式從來沒有啟用警告,所以即使是新程序員也會(huì)繼續(xù)以糟糕的方式做事。
當(dāng)然,我知道這對(duì)于向后兼容性很重要。但這就是整個(gè)問題:C++ 積累了太多錯(cuò)誤的決策。為什么要復(fù)制參數(shù)傳遞集合的默認(rèn)值,更不用說 lambda 捕獲了?我知道歷史原因,但這并不意味著現(xiàn)代編程語言應(yīng)該這樣工作。
即使 C++11 也無法消除這樣一個(gè)事實(shí):原始指針和 C 風(fēng)格數(shù)組具有良好的語法,而智能指針看起來很std::array 糟糕。即使 C++11 也無法澄清它正在圍繞一種無需移動(dòng)而設(shè)計(jì)的語言工作。
四、寫在最后:C++痼疾難消
不幸的是,我非常清楚為什么做出這些決定,而這正是原因之一:與遺留代碼的兼容性。C++ 沒有版本系統(tǒng),無法棄用核心語言功能。如果創(chuàng)建了 C++ 的新版本,它將不再是 C++ ——盡管我支持人們將 C++ 轉(zhuǎn)換為新語法并清理其中一些內(nèi)容的努力。
這也是唯一的好處:與歷史的連續(xù)性。雖然我可以看到其中的價(jià)值,但它的價(jià)值非常有限,范圍也非常有限。但是,如果你忽略掉向后兼容性和現(xiàn)有的大型代碼庫,那么這些“剪紙”技巧都不會(huì)使編程語言變得更強(qiáng)大或更好,只會(huì)更難使用。我看到過支持“人工維護(hù)頭文件”的觀點(diǎn),這讓我很驚訝,告訴我 C++ 在這些問題上的設(shè)計(jì)選擇有什么好處。
有人可能會(huì)說這些事情微不足道,但這些都會(huì)減慢程序員的速度,同時(shí)又讓他們煩惱。如果你有足夠的經(jīng)驗(yàn),你的潛意識(shí)可能擅長駕馭這些“招式”,但設(shè)想一下,這些潛意識(shí)原本是要來注意哪些方面的。
想象一下,你在初級(jí)同事的代碼審查中能很快察覺到這些錯(cuò)誤嗎?如果你是嚴(yán)格審核人,還需要多少時(shí)間?如果這些問題得到解決,開發(fā)者會(huì)變得更加有效、更加高效、更加快樂。編程也會(huì)變得既有趣又快捷。
原文鏈接:https://www.thecodedmessage.com/posts/c++-papercuts/
標(biāo)題名稱:C++做了太多錯(cuò)誤決策!
本文路徑:http://m.fisionsoft.com.cn/article/djpgpij.html


咨詢
建站咨詢
