標籤:

為什麼C++語法這麼複雜?

之前很少用C++,基本上是Java/PHP偶爾用點C#,今天突然意識到自己在C++上一直是C with class……


你可以嘗試設計一門編程語言,包含 C++ 所有功能,而又比 C++ 語法簡單。

為減低難度,可先不考慮兼容 C 語言。


模版帶來的複雜:

It"s risky to design minilanguages that are only accidentally Turing-complete. If you do this the odds are good that, sometime in the future, some clever fellow is going to think he needs to press your language into doing loops and conditionals for him. Because these are only available in an obfuscated way, he"ll produce obfuscated code. The results may be serviceable in the short term, but are likely to be a nightmare for those who come after him.

--- The Art of Unix Programming

C++ 的流行始於 Windows 應用開發的上升期。Windows 對 C++ 的影響:

If an operating system makes spawning new processes expensive and/or process control is difficult and inflexible, you"ll usually see all of the following consequences:

... ...

Lots of policy has to be expressed within those monoliths. This encourages C++ and elaborately layered internal code organization, rather than C and relatively flat internal hierarchies.

--- The Art of Unix Programming

有很多答案用 C compatibility 來 justify C++ complexity。這種 justification 成立的前提是,C compatibility 真正實現了。然而但凡了解 C++ 的人都知道,C++ 並不是真的「兼容」C,而是所有的 C++ compiler 內部都實現了一個 C compiler。而 C++ compiler 中的 non-C 部分只是語法盡量與 C 接近。

The flip side of the Rule of Least Surprise is to avoid making things superficially

similar but really a little bit different. This is extremely treacherous because the

seeming familiarity raises false expectations. It"s often better to make things

distinctly different than to make them almost the same.

--- The Art of Unix Programming


C++語法並不複雜呀


https://m.douban.com/book/subject/10536031/

講得很清楚,C++設計都有實用目的,可能不是最好的設計,但基本是向簡化問題,而不是搞複雜問題的方向。

所以C++比它要解決的問題是簡化了。


C++的確很複雜,語法繁雜得總是讓猿猴不由自主地浮想聯翩為啥要這麼複雜,能否簡單一些,但是功能保持一致,甚至也不要求全部保留,就守住主要特性就行了。而每次的思考結論,基本上所有干過這個事情的無聊猿猴,都承認,一開始就要兼容C語言,引入class概念,還要求新類型的使用方式可以和原生類型一模一樣的直觀,與此同時,還要保留對內存最精細的控制(要在棧上搞對象,要在堆上搞對象,直接訪問,間接訪問,間接間接訪問等等),還要零懲罰,還要靜態強類型,還要異常,還要template(這個東西必須一定要的,靜態語言缺少這貨,就很不好玩了)。那麼,這種語言,基本上就是C++現在的這個樣子了,大同小異。而且,更令人沮喪的是,不搞那麼多複雜東西(比如虛函數、多繼承、異常、template等),一旦開始從C語言做文章(兼容C),開腦洞搞靜態強類型的面向對象,良好優良的設計下,一路發展進化下來,必然勢不可擋就要添加多繼承、操作符重載、析構函數、異常等等雜碎,也就是現在這個鬼樣子的C++了,對此,vczh有過詳細的推演。這好像小說家說的,主角的命運,後期已經不是作者所能左右的了,主角的命運,最後只能是小說的舞台背景勢力來決定。

當然,這並不表示C++現在就完美了,其設計就無懈可擊了。比如,C++下要是能支持非侵入式的成員函數就好了,好比C#下的擴展成員函數;lambda支持參數類型的自動推導,進一步說,有first class的原生函數類型;好比C++能開延遲求值的後門,可以讓猿猴做短路運算求值的文章;又好比,很多時候,C++可以模擬很多語言的語法糖,但是haskell下monad的do糖,在大C++下,就很難搞,這也暴露了C++在支持函數式編程上先天性的不足;……,所有這些,都不太要緊,真的。世界上本就沒有保羅萬有的語言,底層玩得溜,還要玩高抽象;其次,現有的C++,已經可以很輕鬆地做很多很多的東西,差別只是沒有以上的東西,代碼上略顯冗長或者沒那麼優雅或者用起來沒那麼爽。面向對象是很強有力的抽象範式,注意,面向對象不僅僅只是java、C#等支持的那種基於class、interface的靜態面向對象,它還包括消息發送、com式的動態查詢介面、運行時動態修改反射元數據等玩法。某些場合,大C++的寫法的確不如haskell等的優雅,但是其template的強悍和全局變數靜態變數,那也是其他語言無可匹敵的存在。下面,玩一下全局變數的代碼。就拿getsockopt來說,

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t*optlen);

調用時,要傳入好幾個參數,代號,傳入變數的地址,內存佔用大小,很麻煩,具體請百度。當然,C的api也只能搞成這個樣子,已經是極限了。下面看看大C++如何化腐朽為神奇,如何把後面三個緊密相關的參數打包在一起的。

先定義Option Value的Trait。

template&

struct TSocketOption

{

int mName;

TSocketOption(int name): mName(name){}

};

然後,定義全局變數。這個全局變數會在main運行前構造完畢。

const TSocketOption& kSendBuffer(SO_SNDBUF);

接下來,簡單包裝getsockopt,這裡性能損失基本上為0,也就是零懲罰。為求簡單,忽略了錯誤處理。

template&

Ty GetSocketOption(int sockfd, int level, const TSocketOption& opt)

{

Ty val;

int size = sizeof(Ty);

getsockopt(sockfd, level, opt.mName, reinterpret_cast&(val), size);

return val;

}

最後,就可以這樣使用了

auto sendBufSize = GetSocketOption(sock, SOL_SOCKET, kSendBuffer);

連變數的類型都給你推導出來了,更別說類型安全了,是不是很方便呢?謹記,GetSocketOption千萬不要在main之前調用,因為kSendBuffer可能還沒構造。另外,全局變數只是用來登記元數據,之後,就盡量不能再修改了,所以前面要加const。

所以說,雖然說,C++很龐雜,但是發展到現在,現存的語法特性,沒有一條是多餘的,就算是討厭的隱式類型轉換,也有發光發熱的時候。當然,按照某些猿猴的哲學,所有容易用錯的語法,統統都不益保留,語言缺少某些特性,還成了優點啦。語法單獨去掉一兩條不打緊,這也缺,那也缺,更重要的是喪失了語法特性的組合玩法,語言就開始廢了。C++的龐雜,導致其在編譯器開發、網路伺服器、gui、遊戲、操作系統等等,隨便需求怎麼複雜組合,都可以玩得很溜,那麼多的好東西在哪裡,隨手沾來一用,恰到好處,好貼心。代價就是,猿猴要掌握大c++的學習時間,異常漫長,學習過程又似乎不是很有趣。

如果確實嫌C++太複雜,又想堅持底層的操作能力,不保留底層的話,現在市面上隨手一抓,就一大把好語言,java,C#,F#,lisp,haskell,scala,何苦用狗屁不通的go語言。唯一的方式,就是不要兼容C,全部重新設計,從根本上的設計就對template,lambda,面向對象,預處理(宏的能力),操作符重載等進行深思熟慮的思考,但是未必是rust,rust太多限制了,全副心思好像都花在內存管理上了,很讓人煩心,這完全沒必要。而且整個語言的表達力,還很遜色於大C++的,意思就是不如C++強大。


兼容C

兼容舊代碼(看向某python

因為兼容和某些設計上的不周到使得用戶發現某種trick可以達成某項設計本意之外的目的

新版本必須兼容這種trick,甚至在其之上發展

新版本功能又被發現有某種新trick

goto第三行


幾個原因吧:

  • 向前兼容性,特別是需要兼容C。
  • 設計不良,比如關鍵字太少、括弧不夠用造成的歧義;應當做成語言內置功能的非得用模板寫成標準庫。


其實要能把C兼容性砍掉的話,C++語法至少能簡化50%。

然鵝用戶會減少99%。


不懼怕速度的話,Python、PHP適合你。

想要速度,犧牲一點代碼量,Java、Go適合你。

想要對自己程序的完整把控,還要速度,還不怕噁心,就堅持C/C++。

想要噁心自己,JavaScript適合你(滑稽(前端開發激怒(逃


C++有幾個基本原則,比如兼容C、零額外成本,這些增加了C++的複雜度。


《Effective C++》第一條就明確指出學習C++ 要把它看成四種語言的聯盟(原文是federation),也就是1) 兼容的C 2) 面向對象的C++ 3) 模板編程,即元編程 4) 標準模板庫 (STL)

你如果把學習C++當作學習四種語言,你就會少很多困惑,對C++各種不一致的語法設計容忍性也高了。


我覺得C++複雜的語法主要集中在兩個方面:模版元編程,以及內存操作(Memory Manipulation)。

模版元編程的出發點是C++編譯後就不會有元信息了(例如類型),所以需要在編譯階段提前處理。問題是這樣的話你看到的代碼其實是分處於兩個不同層面上的混合物:第一層是編譯期會運行的代碼,包括所有的template、concept、typename、decltype、constexpr等等;第二層是運行時代碼,這個和大家都一樣。在沒有經過相應的訓練前,看這些代碼確實會比較吃力。

內存操作的出發點是性能考量,以及內存釋放的確定性(相較於GC語言的不確定性)。大部分C++代碼里,如果沒有封裝好,可能一半的代碼都是在處理內存的細節,包括但不限於:

1. 在堆上還是棧上還是在DATA segment上分配對象?

2. 對象什麼時候被析構?函數返回的時候,函數執行時根據變數決定,還是程序退出時?

3. 對象的ownership:是留在函數里,還是返回給調用方?

4. 對象的傳遞:是按值傳遞,按地址傳遞,還是移動(T)?

這兩個部分其實對於C++代碼想要表達的業務邏輯沒有幫助,但又很容易混雜在業務邏輯代碼里,增加閱讀難度。如果不得不選擇C++,熟悉一下這些語法,在閱讀中嘗試自動忽略這些和業務邏輯不十分相關的實現細節,大概能更容易理解你要看的代碼。


最重要的原因就是歷史包袱已經兼容性,其他原因都沒有這麼重要。

我想 Dlang 應該是最接近你需求的語言,語法非常簡單,編譯非常快速,元編程能力遠強於 C++,並且其他功能上也並不弱,相比 C++ 缺少的能力很大一部分都是因為必要。真的有需要,你完全可以在 Dlang 的基礎上做出改動,砍掉 gc 引入 C++ 有的你需要的功能。


一直在大學寫一些作業和競賽性質的小項目,作為一個學過但不精通c++的人說一些看法,我認為c++在大多數時候是好寫的,但在真正編譯運行之前我不太能預測會發生什麼樣的問題,如果c++完全不使用指針、宏,不考慮性能的情況下,只使用STL,那語法又能比java更複雜到哪裡去,寫的太複雜是看我代碼的人說的,實際上我在寫的時候我很清楚自己在做什麼。

而對於初學者而言,學習c++最大的成本不一定來自語言本身,而一部分c++程序員不太友好的姿態,往往才是遇到簡單問題的新手最大的障礙。


歷史包袱歷史包袱。一開始c++就是C with class啊,解決c的痛點設計了c++。為了兼容,最重要的字元串string類都不是原生類型而是class,編程語言裡面也是獨一份吧。這時候c++還不複雜,C with class其實這時候編譯器做的事兒還不多,感興趣或者蛋疼的朋友可以試試,假如用純c+宏+函數指針,能模擬c++的OO到什麼程度?還缺了哪些沒法模擬?這些就是c++編譯器做的事兒。我記得就一個this指針,還有class的構建析構尤其是析構。

c的宏還記得吧,c++肯定得繼承,然後發現這個宏還能擴展一下,代碼替換多美好,「泛型」這兩個字相當精確的表達了模板設計的初衷。

然後,發現模板用來替換類型有點大材小用,再加點料;記得到了90年代末期吧,忽然發現:卧槽,c的模板本身已經圖靈完備了。。。。。(gcc上的)。這時候c++就開始鬼畜了。。。

然後c++就從一個兼容c的添加了class的美好語言一路狂飆為一個複雜的,for(;;){c++生成c++再生成c++...}的這麼一個可怕玩意兒。。。。。。

呃,其實有興趣的可以去看看golang,差不多就是我心目中的純粹的「c++」。


我倒是覺得可能是會者不難。

比如我幾次試圖用java和c#,都發現他們相關的各種庫和模式太複雜了。

比如要寫個簡單的爬蟲,我寧願cpp也不想看java

但是我覺得並不是說java真的比cpp難,而是他們應用場景不同。比如要做圖形,三維,可能一個平時用java的人根本不熟悉這些場景。反過來cpp程序員去做java擅長的場景就會面臨很多業務問題。

所以真正讓人覺得難的並不一定是語法。

比如cpp程序員也不會覺得模板特化模板元多複雜。普通水平的可能覺得boost源碼看不懂,但java程序員應該也有很多看不懂的java代碼吧?


無非是兼容性,易用性,高效能。

其實作為萌新,完全不必想著怎麼把cpp學完什麼的,學不完的。

學學基礎的語法,再理解理解內存,就可以開始簡單的工作了。學到c with class,再加上點多態的知識,少少模板的知識,就可以算是會cpp了,大部分工作都能搞定。完全理解模板的,那都到深資階段了。

模板部分相當於詩詞歌賦,會了很牛逼,不會也沒啥,等你在這裡面呆久了,自然是不會作詩也會吟。


因為C++實際上是面向對象、過程以及編譯器編程


每個語言都有陰暗的角落,就像你平時寫Java,也不會大把大把自己寫編譯期註解吧?

寫C++,也不是一片模板元編程吧?

再說了,C++歷史包袱重,zero abstraction又給了很多限制,有時候的確只能做到很彆扭。


對啊,前後兼容,什麼特性都要,這也是cpp牛逼的原因(就算大項目很多特性也用不到吧,用到再查唄),每次新標準都那麼一大堆新特性,一般程序員都無法想像這些是怎麼實現的。心疼寫編譯器的大佬們。


推薦閱讀:

程序員都是怎麼記代碼和編程語言的?
樹莓派為什麼採用python語言為主要開發語言?
Rust 火了會怎樣?
程序員最重要的能力是什麼?

TAG:編程語言 | C | CC |