[轉]爭論C++前你應當知道什麼

爭論C++前你應當知道什麼”

 

By 劉未鵬(pongba)

C++的羅浮宮(http://blog.csdn.net/pongba)

 

最近寫了一篇關於C++0x Concepts的文章,意料之外地引起了一場小規模口水仗。回各位帖子的同時,回想這些年C++社羣的大小爭論,覺得有必要把一些長久以來在C++爭論中出現的誤解列舉出來。

 

注:這篇文章行文匆忙,但觀點不匆忙。匆忙的問題在於可能還沒有列舉出所有的fallacies。所以我在文章標題上加了個rev#1。如果你看了之後覺得有什麼fallacy要補充的,歡迎在回帖內指出。我會考慮加入下一個review版本:-)

 

 

… History became legend, legend became myth …

- The Lord of the Rings

 

哈雷將軍的笑話想必大家都聽過。一句話經口口相傳,每個人都根據自己的主觀意念加以潤色,修補,歪曲到最後就面目全非。這裏最關鍵的一環就是主觀意識,在歷史學裏面有這麼一句話,大致意思是歷史其實只存在於人的意念之中;就算完全客觀的事件,通過不同的人的嘴說出來,造成的心理效應也往往不一樣,每個人都會加上那麼一兩個形容詞,駕馭語言能力高的更是能夠舌綻蓮花,而語言本就有自身的力量,其中的遣詞造句對讀者構成的心理影響力便應運而生。甚至於同一句話,用不同的語氣說出來,都會造成不同的效果。同一句話,站在不同的立場上看,也會根本不是同一個意思。比如“C++還算是門不錯的語言”,站在C++擁護者的角度聽是在憐憫加詆譭C++,而站在C++反對者的角度聽卻是擡舉了C++

 

在一個長期被廣泛爭論的話題中,幾乎無可避免的總是存在一些FallaciesMyths。比如動態&靜態類型系統的爭論,據說圖靈時代就開始了,到現在還有各種各樣的誤解,而且,可以說,時間越長,系統內的Fallacy越多。就連異常(exception)這樣不算複雜的語言特性裏面居然也有一些長期存在的誤解

 

至於這些FallaciesMyths出現的原因很多:有人要“內涵”唬人、有人要維護自己的心理優勢、有人要維護自己的政權、有人要維護自己的利益、有人因爲話從別人那裏聽了半句轉述給別人聽的時候按主觀意念補全(誰願意說“我不知道”呢?)、有人乾脆就是人云亦云

 

所以,一句話,在一個靠口頭表達交換信息的社會中,FallaciesMyths是無處不在的,因爲從內心真實想法到外界表現出來的想法之間存在着“口頭表達”這一中間層,後者由主觀意志支配。這裏的中間層可不比軟件工程裏面的間接層,在這個間接層上惡魔可以變成天使,天使也可以變成惡魔;六月飛雪可以變成天降祥瑞,瓢潑大雨也可以變成豔陽高照。Anyway,這展開來就是一個心理學的問題了,不多廢話了,有興趣的可以去看Harry G. Frankfurt寫的On Bullshit或者Scott Berkun的這篇短文——“How to detect bullshit”。呃我說“一句話”了麼?

 

C++ - Fallacies and Myths

C++作爲一門被爭論不斷的語言,其中FallaciesMyths自然不會少。一般來說,一個問題在被大衆爭論中交換的話語數量與其中的Fallacy數量成正比。但一般來說主要的Fallacies就那麼幾個:

 

Fallacy #1 —— C++社羣的哲學太學院派

讓我們先對“學院派”下一個定義好不好?先問你自己一個問題,你心目中對“學院派”的定義是什麼?

 

以下是一些選項:

 

1. 傾向於理論美。

2. 忽視實際編碼中的constraints(如效率,模塊性、可讀性等等)。

3. 倡導語言律師行爲。

4. 鑽細節。

5. …

 

我想如果我說C++語言設計強調理論美,所有學過C++的人恐怕都會笑了正如Bjarne自己所說的,C++設計初期的Rule of Thumb之一便是“不要陷入到對完美性的固執追求中”;不過具有諷刺意味的是,後面你會看到,正是這樣的一種哲學帶來了今天對C++的這個誤解。

 

我猜持這樣一種觀點的人大多對於學院派的定義都是模糊的,一般都介於“提倡鑽語言細節並利用語言細節的做法”、“關注語言特性本身而忽略實際編碼需求”、“對語言細節無休止的爭論”等等之間。

 

所以,當有人說“C++==學院派”的時候,他的真實意思很可能是:“C++語言的陰暗角落太多,而且C++社羣還有提倡對語言角落把握的潛在哲學,就連C++0x的進化也似乎更多關注語言特性,而那些語言特性根本就跟我們實際開發者脫節了”等等。

 

首先得承認的是,在近一個十年的時間內,C++社羣的確某種程度上建立起了一種對語言細節過分關注的心態,這種心態毫無疑問是錯誤的,但只有知道這個錯誤是如何來的,才能解開這個結。而且,就算一時解不開這個結,知道了原因之後才能保持理性的寬容態度,而不是亂髮抱怨。一個理性的態度,更有助於良性發展。例如如果C++社羣都能明白這種潛哲學從何而來,或許也就會漸漸走向更好的發展了。

 

我用一個例子來說明這一點:你平時遍歷一個數組,或一個容器的時候是怎麼做的?

 

for(std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {

}

 

這種做法很臃腫。其實你的邏輯是“對v中的每個元素,做事情”,你知道大多數其它流行的語言中都有內建的for_each。那C++中就沒有了嗎?有。STLfor_each算法,於是你寫:

 

struct MyOp

{

void operator()(int& i)

{

}

};

 

std::for_each(v.begin(), v.end(), MyOp());

 

這個方案實際很差。一是你還是得寫v.begin()v.end(),二是你得爲此定義一整個新類。三是這個新類並不在你使用這個新類(for_each被調用)的點上,因爲局部類不能做模板參數。

 

你要的是lambda function

 

for_each(v.begin(), v.end(), <>(int& i){ … });

 

可是C++98沒有。

 

你要的是內建foreach

 

for(int& i : v) {

}

 

可是C++98沒有。

 

鑑於循環結構是編程中最常出現的結構之一。這個問題其實還是比較惱人的,如果你覺得不惱人可能只是因爲你適應性習慣了,這未必是好事。比如每次都要寫std::vector<int>::iterator就很讓人惱火,如果我換個容器,就要修改一堆std::vector<…>。那用typedef行不行啊?行。可仍然還是需要寫一次typedef,我很懶,我什麼多餘的無用代碼都不想寫。要知道,每多出一行無用的(並非因表達思想所需要纔出現)的代碼,就增加一點維護負擔,這也正是爲什麼語言的表達力如此重要的原因。

 

那怎麼辦?如果我告訴你,C++98裏面其實你也可以寫:

 

foreach(int& i , v){

 

}

 

你怎麼想?

 

廢話。當然是求之不得了。有這麼簡潔的表達方式誰還不想用啊。

 

我需要告訴你的另一個事實是。爲了在C++98裏面幾近完美地實現這個特性,有人把標準的角落挖了個底朝天。不,我不是在爲鑽語言細節找理由,我只是想告訴你,許多人所認爲的鑽語言細節的做法,其實一開始大多是由用戶實際需求驅動的,這個foreach設施被C++程序員們試圖實現了NN種做法,可見需求之強烈。可惜絕大多數實現都遠遠稱不上好用,就連現在這個實現的作者也早在03年在CUJ上發了一個實現,也稱不上好用。是後來又契而不捨才實現了最終這個真正好用的版本的。

 

我想說的是,上面這個美好的foreach,當然人人都想用。但問題是要在C++98下實現它只能靠挖標準,這是唯一的途徑。要不然就得等語言進化,並忍受若干年,誰願意?況且這個foreach設施還能作佔位符,在C++09來臨之前兢兢業業履行其職責,C++09加入內建foreach支持之後只消用正則表達式搜索全局替換,就OK了,沒有任何的升級麻煩。

 

再舉一個經典的例子:STL裏面的traits。其實traits不應該是traits。traits最自然的實現方式應該是C++09的concept。但STL需要用到靜態dispatch技術啊,那怎麼辦?要麼用traits(增加語言複雜性),要麼不用(顯然不行)。

 

再舉個經典的例子:模板元編程。模板元編程有啥用?日常開發者八輩子估計也用不到。但真的嗎?沒錯,日常開發者並不會直接用到但是,由模板元編程支持的各個boost子庫呢?被選入C++0xTR1的各個子庫呢間接用到)?那日常開發者用不用學模板元編程呢?不用學,根本不用學,這麼複雜的技術學什麼呢?也就是點技巧上的東西。那爲什麼偏有人學呢?待會再說。

 

還有大量的例子就不一一列了。其實STLtraits技術已經能夠說明問題了。如果你仔細看一看,你會發現,那些所謂的利用C++黑暗角落的技術,幾乎無一不是出現在庫開發裏面的,而之所以出現在庫開發裏面,是因爲庫開發中的需求驅動的——爲了開發出更好的庫。難道你不想用更好的庫?

 

哦,說到“更好的庫”,肯定會有同學有意見了。C++98都快十年了,標準庫還是隻有那一套STL。庫進展緩慢,到現在GUI庫也沒有一個標準,都是四分五裂各自爲營。網絡庫也是、文件系統庫也是、日誌庫也是不過這個問題已經是另一個問題了,容後再說。

 

問題是,“沒有標準的庫”並不意味着“C++的庫不好”,後者也並不意味着“那些晦澀的技巧並沒有提升庫的質量”,這個邏輯上的兩環都不對。實際上,人們所謂的“晦澀而複雜的技巧”其實正是爲了提升庫的質量而被挖掘出來的traits技術提升庫的效率(靜態轉發),type erase技術使得boost::function<void()>可以接受任何簽名爲void()的函數(靈活性),包括仿函數,包括boost::bind後的函數。type list技術使得boost::tuple能夠接受可變數目的模板參數。policy-based design使得可以對一個設施的功能進行正交分解

 

就算把所有流行的C++ tricks都列出來,你也會發現,其實它們幾乎每一個都對應了至少一個實際應用。而實際應用需求哪來的?庫設計的需求。但歸根到底,是使用庫的人——終端程序員——的需求。(效率、靈活性、抽象表達力,哪一樣不是終端程序員的實際需求呢?)

 

再舉個實例,有同學說,我只要寫簡單的代碼。問題是,簡單不意味着單純。簡單意味着在更高抽象層次上面編程,後者是要靠好的庫抽象才能達到的。借用《Extended STL》裏面的一個例子:

 

DIR*  dir = opendir(".");

if(NULL != dir)

{

  struct dirent*  de;

  for(; NULL != (de = readdir(dir)); )

  {

    struct stat st;

    if( 0 == stat(de->d_name, &st) &&

        S_IFREG == (st.st_mode & S_IFMT))

    {

      remove(de->d_name);

    }

  }

  closedir(dir);

}

 

這段代碼刪除當前目錄中所有文件。

 

readdir_sequence entries(".", readdir_sequence::files);

 

std::for_each(entries.begin(), entries.end(), ::remove);

 

這段代碼做同樣的事情——哪個更簡單?

 

那問題是,爲什麼發展到後來,“鑽語言細節”成了社羣的潛在哲學呢?

 

這其實是一個心理學上的問題,跟語言沒有關係,跟C++的初衷更沒有關係。從心理上,在同一個領域,如果另一個人比你懂得更多,你就會傾向於佩服他,這時另一個人懂的東西有多大的用處其實並不那麼重要,人對自己不懂的東西總是有一種敬畏感的C++裏面有那麼多的tricks,其實日常編程中要用到的trick少之又少,日常編程絕大多數都以複用庫爲主,而那些tricks就隱藏在庫裏面。除非你是庫的設計者,否則很多的tricks根本就無需關注。另一方面,寫作C++書籍的大多數都是C++庫的設計者,這就給予了許多C++書一個有偏見的視角,大量庫設計中才會用到的技術被介紹出來,而社羣對這些牛人又都是唯馬首是瞻的。(其實我覺得一本Bjarne的《The C++ Programming Language》加上一本Herb&Alexandrescu的《C++ Coding Standard》對於日常程序員來說,真的足夠了。)

 

此外,人總是好奇的,在C++裏面有那麼多的被“發明”的好玩技術,怎麼可能不會有人去追捧呢。另一個動機則是學了這些技術有立竿見影的炫耀效果,比如在論壇上。這可比編寫可維護代碼的才能容易表現多了——人自然是更傾向於去關注那些更容易拿來表現和炫耀的東西的。退一步說,就單單是“發明”一項新的語言特性組合運用技巧都能帶來純粹的成就感,因爲你又在現有語言框架下作出了一個創舉,你做了別人做不到的。而作爲學習者,你可能會因爲發現原來自己理解的一塊土地上還有不知道的東西而感到興奮和新奇,這種興奮和新奇感往往是學習的真正原動力。至少,對於我來說,當年讀《Modern C++ Design》時正是這樣一種感覺,我想有和我一樣感覺的人肯定不在少數。

 

再來,一個是在人前看不見摸不着的編碼能力,另一個是對handy的技巧的掌握。作爲一個學習者,傾向於學習後者,因爲後者學起來更容易,而且也往往更有趣。學到了之後能夠得到跟解決重大問題同等的成就感——看看《Effective C++》系列受到的追捧就知道了。

 

再來,當你面臨兩個問題,一個是如何建立一個高質量的庫(大),一個是如何修正庫裏面的小bug(如vector裏面某個成員函數的異常保證問題)。如果你有一份時間,你更傾向於把它花在什麼地方?人在心理上總是傾向於走“捷徑”的,體現在這個問題上面便是更傾向於對付耍點小聰明就解決的小問題,並獲得甚至並不亞於解決大問題的成就感。小問題的另一個吸引人的地方在於它耗時短,更“趁手”,它不需要你閉關苦苦編碼幾個月弄出一個框架來而且還不一定能成。所以這就給人一種錯覺,C++社羣只知道爭論枝節問題,不知道實幹庫。哦,不是錯覺,這的確是大部分的現狀,但這個現象其實並不僅僅止於C++社羣,這是人心理的共性造成的,這也就是爲什麼無論在哪個語言社羣你都會看到爭論最多的都是些“小問題”的原因。(當然,無論在哪個學科,也還總是有牛人去啃難啃的骨頭的。但這並不是廣大民衆的狀況。)

 

以上種種原因共同造就了C++社羣的這種心態。這其實跟C++的“教義”沒有關係。C++如果有教義的話也是實用爲上。這種現狀是自發產生的,它的動力來源於人的心理。如果Java語言有各種各樣的特性組合,且這些特性組合能夠某些時候滿足開發中的實際需求的話,也是一樣會出現這樣的情況的(事實上一個小小的Java Closure就已經引起了大量口水了)。某種程度上,LISP裏面用函數來實現自然數系統,也是一樣的道理,你敢說,你第一次看到它的時候,不驚歎?人之常情而已。

 

那這種哲學對不對?廢話。當然不對。不但不對,而且有害。很多C++日常開發者在學習庫開發技巧上浪費了很多時間,掌握了根本用不到的技術,而且這些技術,不如稱爲技巧,可能還會隨着語言進化變得根本無用武之地。還不如好好學學如何讓自己的代碼更KISS呢,基本的編碼準則要遠遠重要得多,正如我剛纔說的,日常開發,一本The C++ Programming Languag加一本C++ Coding Standard足夠了。

 

另外,說到語言進化順便說一句,語言進化的職責之一便是廢黜繁複的技巧,取代以直接表達思想的語言特性。而C++0x真正在履行這一職責

 

最後來說一說前面留下來的一個問題:爲什麼C++設計的初衷——“不要固執於完美”——某種程度上帶來了這個局面呢?

 

因爲正是因爲這種理念的指導,有不少語言特性從理論上都是不完備的:比如有copy語意沒有move語意(有左值引用沒有右值引用),於是AlexandrescuMojo框架來解決;比如支持可變參數的函數調用卻不支持可變參數的模板參數列表,導致用元編程來解決;比如不支持構造函數轉發,導致必須factor出一個公共的initialize函數來;比如不支持強類型的enum,結果用一大堆宏結合類來解決;比如不支持initializer list,結果複雜的模板技術來實現某種類似的初始化方式;比如不支持autotypeof,結果用更復雜N倍的模板元編程技術來實現一個模擬;比如不支持內建的alignment指示,導致Alexandrescu在實現類型安全的union的時候用盡了模板元編程技巧;比如不支持內建的foreach,結果藉助於詭異的語言角落實現了一個幾近完美的模擬;比如不支持內建的concept,導致使用模板技巧來實現也算能用的concept檢查這個列表可以一再延長下去,C++中這樣的示例太多了。C++的不完美導致了各種各樣的技巧應運而生,哦,不,應該說,應實際需求而生。這從另一個側面正說明了一點——

 

C++太需要進化了!

 

Fallacy #2 —— C++委員會過分關注一些不切實際的語言特性,而不關心標準庫的擴充

比起第一個fallacy來,這個倒容易解釋清楚了。人家Bjarne在文章和訪談裏面一再強調,C++從來都是把庫設計放在首位的(這句話其實就意味着,是把最終開發者放在首位的——什麼?你難道不用庫?),但是C++羣體是一個分散多樣的羣體,而且沒有大公司的財力支持。前者意味着衆口難調(標準化過程困難),後者意味着不能集中精英的人力boost庫的開發都是由大家業餘時間完成的)來搞出個百萬美元的免費庫來此外個人用業餘時間來開發庫還意味着往往沒有足夠的精力來對庫進行精化改善,導致庫的質量不佳或者乾脆停滯(這樣的C++庫案例很多)比如日誌庫吧,沒有一打也有半打,但由於都是個人業餘開發,所以沒有精力做到盡善盡美,唯一一個往boost提交的是John Torjo(也是個牛人)寫的,不過一年前被reject之後就沒了動靜。你難道怪人家?人家又不是你僱來的。

 

說到底,還是錢的問題,衆口難調還是終究能調的(boost發起的初衷便在於此)。但沒有錢,鬼才跟你推磨呢

 

不過好消息是據說boost明年能拿到fund:-) 應該能把boost狠狠boost一把。

 

至於“C++委員會過分關注一些不切實際的語言特性”就不知從何說起了。首先,前文已經明確說明語言進化的重要性以及實用性,這說明語言進化根本不像人們認爲的那樣“不切實際”,而是與實用休慼相關的。其實從根本上,語言進化就是爲了帶來更好的庫,以及更好的代碼(包括日常編碼),這一點跟大夥殷殷企盼着標準庫其實並不相左。此外還有一點就是,討論語言特性比實際去開發庫要花更少的精力,這兩者花的精力其實不在一個數量級上,開發一個庫出來要難得多得多,所以就造成了一種假象——“委員會的那幫傢伙只知道倒騰語言”。這個論點錯在了兩個地方,一,倒騰語言是必要的。二,他們並非只知道倒騰語言,只是庫的問題要艱難得多,沒錢,人家難道砸鍋賣鐵給你開發標準庫嗎?

 

有同學說,我只要一個能用的庫就行了。但問題是,標準庫能隨便嗎?標準庫之所以不能隨便,是因爲像這樣受衆極其廣泛的庫可是要負責任的——將會有百萬千萬行代碼都依賴它。如果標準庫裏面有bug,將會出現幾百萬上千萬行workarounds,這些workarounds依賴於庫的bug,爲了保持向後兼容性,標準庫甚至都不能修正這些bug。就連STL這樣漂亮的抽象,迭代器區間還是闖了禍。另一方面,如果只是需要一個能用的庫,C++社區有大量“能用”的庫。姑且不說boost裏面的了。

 

Fallacy #3 —— C++的強處在於什麼都能做

一個最常見的論調就是,java的虛擬機也是C++做的,於是得出結論,javaC++弱,java沒有C++好。

 

姑且不說“好”的定義標準是什麼。就算java的虛擬機做的,那C++的第一個編譯器還要用C寫呢。C庫裏面的某些成分還要用匯編寫呢。這個論據是站不住腳的。

 

其實,持這種論點的人是站錯了位置,問錯了問題

 

關鍵的問題不是一門語言能做什麼,因爲說到能做什麼,彙編什麼都能做。而是“在某個特定的領域,哪門語言表現更好”,人們的需求幾乎總是對着某個特定的領域的。後者纔是真正matter的問題。

 

從這個角度看,C++的市場其實只在效率這一塊。有人可能會說,那效率這一塊有C啊。問題是,C的抽象機制太弱。寫架構簡單的應用,或者寫一些核心的(如驅動程序),沒有面向對象結構的程序,容易。完全可以用C。但涉及到大型系統,比如.NET基層架構,一些3D遊戲。必須用到面向對象或基於對象編程的領域,C在代碼組織和抽象方面的弱點就暴露出來了。比如用C和宏來實現所謂OO,就正說明了C的抽象機制的薄弱。

 

但是,C++的領土基本也就在這些地方了。簡而言之就是所有“效率重要,且同時需要好的抽象機制的應用領域”。因爲C++的優勢就是無損效率的實現更好的抽象。

 

C++既有效率又有更好的抽象機制,爲什麼C++不能取代java、不能取代python,不能取代ruby?或者至少當C++進化到更好的階段的時候,比如C++0x就是一個大的進展(在語言方面),爲什麼作爲一門語言,不能取代那些嚴重“偏科”的語言呢?

 

原因有兩方面。一方面,正因爲“偏科”,所以有些語言才能在它們擅長的領域做得更好,乃至做到最好。“偏執狂才能生存”。人們的需求幾乎總是在特定領域的,你說這時候人是願意選用一門專門爲這個領域而生的語言(ruby),還是願意用一門general-purpose的語言(C++)?另一方面,就算C++在抽象機制上進化到了非常好,乃至於能在某些特定領域也表現不菲的話,由於市場早就被別的語言侵佔,別的語言已經有了成百上千萬行的代碼基,別的語言的庫已經發展到非常豐富的程度,別的語言的相關人才教育已經一代又一代,所以結果還是沒得拼。

 

其實,從另一個角度來說,C++何嘗不也是一門偏執的語言呢C++的偏執就是效率,C的偏執也是效率,但C++提供更好的抽象,因此在這一塊(效率+抽象),C++C有優勢。

 

C++的領土已經鑄成,另一方面,C++的領土在可見的未來也不大可能縮水了。這是C++的現實,這個現實,至少在Bjarne看來,也沒什麼不好,因爲它正反映了C++當時設計的意圖——更好的C。我們也不用趕鴨子上架,非拿C++和其它語言比——適用的場合本就不同,沒得比。

 

Fallacy #4 …

事不過三,就此打住。況且,這三條難道還不夠嗎?如果你想到還有什麼fallacy要補充的,請不吝回帖:) 我會考慮加到文章裏面的:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章