劉未鵬(pongba) /文
C++的羅浮宮(http://blog.csdn.net/pongba)
問題
為什么用C++呢? 在你皺著眉頭離開之前,試著回答這個簡單的問題。效率,是么?人人都知道這個。但情況是,當一個人開始討論編程語言或與其相關的話題時,他必須要非常明確而有針對性。為什么呢?我來問你另一個問題:如果效率是人們使用C++的唯一理由,那么為啥不直接用C呢?C被認為比C++效率更高(嗯嗯,我知道C沒有比C++的效率高多少,所以這里別誤解我的意思,因為即使它們二者效率相同,剛才的問題依然存在)。
迷思
我知道你又要說“更好的抽象機制”了,因為畢竟C++是要設計成一個更好的C的。C++沒有犧牲效率,同時又添加了這么多高級特性。但問題是,“開發者們真的需要這些高級特性么?”。畢竟我們一直聽人講KISS(Keep It Simple and Stupid)之類的東西。我們也都聽到有聲稱C比C++更KISS所以我們要用C云云。這種持續不斷的爭論將C與C++之間的比較變成了一個大大的迷題(或者說是混亂)。令人驚訝的是,貌似的確有很多人更加傾向于用C,最大的理由就是C++實在是太難用對了。甚至Linus也這么想。
這種現象最大的影響就是當人們在C和C++之間權衡時,使人們傾向于使用C。而且一旦人們開始用C,他們很快就適應并滿足了(其實,在任何語言乃至任何人類活動中都有此現象,C++亦然,比如常常聽到有人說“XX語言我用了這么多年,一直用得好好的”,照這種說法任何圖靈完備的語言還不都是能用來編程?)。于是即使他們還沒有試試C++,或者他們還沒成為好的C++程序員時,他們就開始聲稱C比C++更好了。然而其實呢,真實的答案往往總是取決于實際情況的。
我說過“取決于實際情況”了么?那到底實際情況是什么呢?顯然,有些領域C是更好的選擇。例如設備驅動開發就不需要那些OOP/GP技巧。而只是簡單的處理數據,真正重要的是程序員確切地知道系統是如何運轉的,以及他們正在做什么。那么寫操作系統呢?我本人并沒有參與任何操作系統的開發,但我讀過不少操作系統代碼(大多是unix的)。我的感覺是操作系統很大一部分也不需要OOP/GP。
但是,這就表示在所有效率重要的領域,C都是比C++更好的選擇么?未必。
答案
讓我們一個一個來分析。
首先,當人們關注效率時,有2種效率——時間效率(例如OS,運行時庫,實時應用程序,high-demanding的系統)和空間效率(例如各種嵌入式系統)。但是,這樣的分類并不能幫我們決定用C還是C++,因為C和C++的時空效率都很高。真正影響選擇語言的因素是業務邏輯(這里的“業務邏輯”并非表示“企業應用業務”)。例如,使用OOP/GP來表達邏輯(或者說代碼的結構)好呢,還是就只用數據和過程好呢?
據此觀點,我們可以把應用程序大致分為兩類(當然前提是關注的是C/C++而不是java/C#/ruby/erlang等等):底層應用程序和高層應用程序。這里底層是指像OB/OO和GP沒啥用處的地方,其余歸到高層。顯然,在所有C/C++應用的領域(這些領域需要C/C++的效率),屬于高層的應用有很多(可以看看Bjarne Stroustrup在他主頁上的列表)。在這些領域中,抽象至少是和效率一樣重要的。而這些正是C++適用的場合。
等等還有。即使在程序員不需要高級抽象的領域,也不是就絕對用不到C++的。為啥呢?僅僅是因為你的代碼中沒有用類或模板并不意味著不能用以類或模板實現的庫。因為有如此眾多方便的C++庫(還有即將到來的tr1/tr2),我覺得有充分的理由在這些領域中使用C++——你可以在編碼時僅使用C++中的C核心(以任何你喜歡的方式來KISS),同時還能用強大的C++庫(比如STL容器、算法和tr1/tr2的組件)。
最后,我認為人們還常常忽略了一點——有時KISS也是建立在抽象上的。我覺得Matthew Wilson在他新書《Extended STL,卷1》的序言中對此做了很好的闡釋。他寫了2段代碼,一段用C,另一段用C++:
C:
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);
}
C++:
readdir_sequence entries(".", readdir_sequence::files);
std::for_each(entries.begin(), entries.end(), ::remove);
而在C++09里面更簡單:
std::for_each(readdir_sequence(".", readdir_sequence::files), ::remove);
也就是說,我認為即使一個人在自己的代碼里不需要類或模版,他也有理由用C++,因為他用的那些方便的C++庫用到了類和模板。如果一個高效的容器(或智能指針)能把你從無聊的手動內存管理中解放出來,為啥還要用那原始的malloc/free呢?如果一個更好的string類(我可沒說std::string,地球人都知道那個不是C++中能做出的最好的string類)或正則表達式類能把你從一坨一坨的、你看都不想看的處理字符串的代碼中解脫出來,那么為啥還要手動去做這些事呢?如果一個 "transform"(或"for_each")能夠用一行代碼把事情漂亮搞定,為啥還要手寫一個for循環呢?如果高階函數能滿足你的需要,那么為啥還要用笨拙的替代方法呢?(OK,我知道,最后兩個需要C++加入lambda支持才真正擺脫雞肋的罵名——這正是C++0x的任務嘛)
總之,我認為KISS并不等同于“原始”;KISS意味著用最適合的工具來做事情,這里“最合適”的意思是工具能夠幫你以盡量直接簡潔的方式來表達思想,同時又不降低代碼的可讀性,另外還保持代碼容易理解。總之,我認為KISS并不等同于“原始”;KISS意味著用最適合的工具來做事情,這里“最合適”的意思是工具能夠幫你以盡量直接簡潔的方式來表達思想,同時又不降低代碼的可讀性,另外還保持代碼容易理解。
真正的問題
人們可能會說,相較于被正確使用而言,C++(遠遠)更容易被錯誤使用。而相比而言,C程序的復雜性更容易管理和控制。在C++中,一個普通程序員很可能會寫出一堆高度耦合的類,很快情況就變得一團糟。但這個其實是另外一個問題。在另一方面,這種事情也很可能發生在任何一門面向對象語言中,因為總是有程序員在還沒弄懂什么是HAS-A和IS-A之前,就敢于在類上再寫類,疊床架屋的一層一層摞上去。他們學會了在一門特定的語言中如何定義類,如何繼承類的語法,然后他們就認為自己已經掌握了OOP的精髓了。另一方面,這一問題在C++中更為嚴重,因為C++有如此眾多的偶然復雜性在阻礙設計;而且C++又是如此靈活,很多問題在C++中都有好幾種解決辦法(想想那么多的GUI庫吧),于是在這些選擇中進行權衡本身就成了一個困難。C++中的非本質復雜性是其歷史包袱使然,而C++0x正是要努力消除這些非本質復雜性(在這方面C++0x的工作的確做得很不錯)。對于設計來說,靈活性不是個壞事情——可以幫助好的設計者作出好的設計。如果有人抱怨說這個太費腦細胞了,那可能是這個設計者本身的問題,而不能怪語言??赡芫筒辉撟屗麃碜髟O計。如果你擔心C++的高級特性會把你的同事引入歧途,把項目搞砸,那你也許應該制定一份編碼標準并嚴格推行(或者你也可以遵循C++社群這些年積攢下來的智慧,或者在必要時,只使用C++中的C或C with class那部分),而不是因為有風險就躲開C++(其實這些風險可以通過一些政策來避免的),因為那樣的話,你就沒法用那些C++的庫了。
另一方面,其實一個更為重要的問題是一個心理學問題——如果一門語言中存在某個奇異的特性或旮旯,那么遲早總會有人發現的,總會有人為之吸引的,然后就使人們從真正有用的事情中分心出來(這有點像Murphy法則),更不用說那些有可能對真正問題帶來(在某種程度上)漂亮的解決方案的語言旮旯了。人們本性上就容易受到稀有資源的誘惑。奇技淫巧是稀有資源,于是奇技淫巧便容易吸引人們的注意力,更別說掌握一個技巧還能夠讓那人在他那圈子里感覺非常牛了。退一萬步,你會發現,即使是一個廢柴技巧也能引起人們足夠的興趣來。
C++中有多少陰暗角落呢?C++中又有多少技巧呢?總的來說,C++中,有多少非本質復雜性呢?(懂一定C++的人一定知道我在說什么)
平心而論,近年來(現代C++中)發現的大多數技巧或(如果你愿意稱之為)技術實際上都是由實際需求驅動的,尤其是需要實現高度靈活而又普遍適用(generic)的類庫 (例如boost中的那些玩意)。而這些技巧也的確(在某種程度上)提供了對實際問題的漂亮解決方案。讓我們來這么想一下,如果你處于一個兩難境地:要么用那些奇技淫巧來做點很有用的東西,要么不做這樣其他人也就沒得用。你會如何選擇呢?我知道boost的英雄們選擇了前者——不管多么困難多么變態多么齷齪,把它做出來!
但所有這些爭論都不能改變一個事實:我們理應享有一個語言,能夠讓我們用代碼清晰的表達思想。以boost.function/boost.bind/boost.tuple為例,variadic templates可以大大簡化這幾個庫的實現(減至幾乎是原先1/10的代碼行數),同時代碼也(遠遠)更加簡潔易懂。Auto,initializer-list,rvalue-reference,template-aliasing,strong-typed enums,delegating-constructors,constexpr,alignments,inheriting-constructors,等等等等,所有這些C++0x的特性,都有一個共同目的——消除語言中多方面的非本質復雜性或語言中的尷尬之處。
正如Bjarne Stroustrup所說,很顯然C++太過復雜了,很顯然人們被嚇壞了,并且時不時就不用C++了。但“人們需要相對復雜的語言去解決絕對復雜的問 題”。我們不能通過減少語言特性而使其更加強大。復雜的特性就連模板甚至多繼承這樣的也是有用的——如果你正好需要它們,而且如果你極其小心使用,不要搬起石頭砸自己的腳的話。其實在所有C++的復雜性當中,真正阻礙了我們的是“非本質復雜性”(有人稱之為“尷尬之處”),而不是語言所支持的編程范式(其實也就3個而已)。而這也正是我們應該擁抱C++0x的重要原因,因為C++0x正是要消除那些長期存在的非本質復雜性,同時也使得那些奇技淫巧不再必要(很顯然,目前這些技巧堆積如山,翻翻那些個C++的書籍,或者瞅瞅boost庫,你就知道我在說啥了),這樣我們就能夠直觀清晰的表達思想。
結論
C++難用,更難用對。所以當你決定用它時,要小心,要時刻牢記自己的需求,所要達到的目的。這里有一個簡單的指南:
我們需要高效率么?
如果需要,那么
我們需要抽象么(請仔細思考這一點,因為很難評估使用C++高級特性是否能夠抵消誤用這些機制的風險;正確的回答取決于程序員的水平有多高,遵循哪種編碼標準以及編碼標準執行得如何,等等)?
如果是,那么
用C++吧。
如果不是,那么,
我們需要用C++庫來簡化開發么?
如果是,那么
就用C++吧。但同時必須時刻牢記你在做什么——如果你的代碼不需要那些“漂亮的”抽象,那就別試圖使用以免陷入其中。別只是因為你在.cpp文件中寫代碼以及你用的是C++編譯器就要用類啊、模板啊這些東西。
如果不是,那
就用C。不過你又會想為啥不僅僅使用C++中屬于C的那部分核心呢?還是老原因:人們很容易就陷入到語言的“漂亮”特性中去了,即使他們還不知道這些特性是否有用。我都記不清有多少次自己寫了一大堆的類和繼承,到最后反倒要問自己“要這么些個類和繼承做什么呀?”。所以,如果你能堅持只用C++中C或C with class的那部分,并遵循“讓簡單的事情保持簡單”的理念;或者你需要把C代碼遷移到C++中來的話,那么就用C++吧,但要十分小心。另一方面,如果你既不需要抽象機制,也不需要C++庫,因為事情非常簡單,不需要方便的組件例如容器和字符串,或者你已認定C++能夠給項目帶來的好處微乎其微,不值得為之冒風險,或者干脆就沒那么多人能用好C++,那么可能你還是只用C的好。
底線是:讓簡單的事情保持簡單(但同時也請記住:簡單性可以通過使用高級庫來獲得);必要時才使用抽象(切記不可濫用;遵循好的設計方法和最佳實踐)。