問題
為什麼使用C++?在你皺眉准備關掉這個網頁之前,試著回答這樣一個簡單的問題。
答案是效率,是嗎?每個人都知道答案。但是,我們應該以更專業的角度來討論一種編程語言或是與之相關的事情。那麼,讓我再問你一個問題:效率是否是人們選擇使用C++的唯一理由,為什麼他們不用C呢?C的效率公認比C++高(當然,我知道,現已證明在某種程度上說,C並不比C++高效,但請不要在此挑錯,因為即使他們是等效的,問題仍然存在)。
神話
我知道你可能會說,這是一種“擇優選擇”,因為畢竟C++就是設計成了C的優化,是C的擴充,可能它沒有想象中的那麼高效,但同時它卻有很多夢幻的高水平的特征。那麼問題就歸結為“開發者真的需要這些夢幻特征嗎?”我的意思是,畢竟我們都聽說過KISS(Keep It Simple,Stupid!保持簡單)和stuff(材料),我們也都聽過這種說法——與C++相比,C更KISS,所以我們應該選擇C。這樣無休止的爭論使得C和C++之間的比較變成了一個神話(或者是一片混亂)。令人驚訝的是,似乎很多人傾向於C,而理由是C++太難正確使用了。甚至是Linus也這麼想。
這種現象產生的真正嚴重的影響是,驅使更多的人在C與C++之間權衡利弊的時候,他們選擇了C;一旦他們開始使用C,他們很快就會感到滿足和舒服,就是所說的“令人滿意”的體驗。這樣,當爭論產生的時候,他們就會站出來說與C++相比,C是更好的選擇。而實際上,他們都沒有真正試著使用過C++,或者他們根本不是足夠好的C++程序員。而真實的答案,往往開始與“它取決於”。
那麼,我說過“它取決於”,取決於什麼?顯然,在一些領域選擇C比C++更好。例如,設備驅動程序的開發通常就不需要OOP/GP(面向對象程序設計/概念編程)技術。它只需要簡單的數據操作;最重要的是,程序員能正確的知道系統如何工作,以及他們該做什麼工作。再考慮OS(操作系統)的開發,我自己從來沒有參與過任何OS的開發,但是讀過大量OS代碼(大部分是Unix),我感覺很多OS重要部分的開發也都不需要OOP/GP技術。
但是,這就意味著,在所有強調效率的領域,C都比C++好嗎?實際上不是。
答案
讓我們具體問題具體分析
首先,當人們關心效率的時候,實際上就關心兩類效率——時間效率(例如:OS,運行時間,實時軟件,高要求系統)和空間效率(例如:所有嵌入式系統)。但是,這種分類並不能真正幫我們決定應該選擇C還是C++,因為C和C++在時間和空間上都是非常高效的。真正影響我們選擇哪種語言(當然是在C和C++之間)的是商業邏輯(這裡的“商業”並不是指“企業應用商業”)。例如,是不是使用OOP/GP來表達邏輯更好,或者是不是除了考慮數據和程序還應該考慮保持軟件美觀。
從這點上來說,我們可以模糊地把應用分為兩類(當然前提是我們只關心C/C++,不關心java/C#/ruby/erlang等):低水平應用和高水平應用。低水平應用的意思就是,在這裡並不需要那些夢幻抽象如OB(基於對象)/OOP和GP;高水平的意思當然就是需要了。顯然,在所有需要C/C++的領域(由於它們的高效性)裡,有大量“高水平”應用(參看在Bjarne Stroustrup主頁上列出的),在這些領域,C++就會更有用。
不過,換個角度想想,即使在這些領域,程序員在他們的代碼中不使用那些高水平的抽象,還是有他們應該使用C++的理由。為什麼呢?因為你的代碼不使用類和模板並不意味著不使用類庫。考慮所有便捷的C++類庫工具(即將有校准擴展tr1/tr2)的實用性,我認為在這些情況下,有非常充分的理由選擇C++——編碼的時候你可以仍然使用C的形式(以任何你想要的方式來保持KISS)。同時,你還可以使用強大的C++類庫(例如,STL標准模板庫,tr1/tr2組件等)。最終,就會發現這件可能會被很多人忽略的事情——有時KISS依靠抽象。我想,Matthew Wilson在他的新書“Extended STL,Vol1”的序言中,極其透徹地闡明了這個觀點。書中提到了兩段代碼,分別用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);
And it’s even simpler in C++09:
// C++09代碼
std::for_each(readdir_sequence(".", readdir_sequence::files), ::remove);
我想,這能很清楚地說明,為什麼即使人們不需要使用類和模板的時候還是應該使用C++——你會發現便捷的C++類庫是多麼有用。類似地,如果一個高效的容器(或者一個巧妙的指針)可以使你擺脫所有手工操作內存的麻煩工作,那麼,還有什麼理由要使用那些原始的malloc/free?如果一個好的string類(我不是在說std::string;人人都知道這不是C++做的最好的)或者regex類可以使你擺脫所有你看都不想看的混亂的字符串處理代碼,那麼還有什麼理由要選擇手動做這件事情呢?如果一個“transform”(或‘for_each’)語句可以如此簡單明了地一行就完成你的工作(當然,我知道C++做這件事需要lambda函數的支持),那麼,還有什麼理由要手動寫for-loops循環?如果高定制的函數真的能實現你想要的功能,那麼,還有什麼理由使用笨拙的工作區來完成同樣的事務呢?
KISS並不意味著“粗糙”;KISS的意思是為你的工作選擇最適合的工具,“最適合”意味著你所使用的工具能盡可能直接(和簡潔)的幫助你表達你的想法。只要它不影響代碼的可讀性和易懂性。
現實問題
人們可能會說C++很容易會用錯,而相反,C通常更容易管理和操控。一個中等水平的C++程序員可能會寫出一大串聯系緊密的類,而很快這些類就會變成一堆垃圾。但這實際上只是個別情況。一方面,在任何面向對象語言中都會經常發生這樣的事情。總是有一些程序員,他們敢於在類之上再定義類,而他們甚至還不知道什麼是HAS-A和IS-A;他們學習了所有定義一個類和從其他類繼承一個類的語法,他們就覺得掌握了OOP的本質。另一方面,為什麼問題會出現在C++中,是因為C++有很多阻止設計的復雜之處,是因為C++如此靈活,以至於每個用C++解決的問題都有很多可選的解決方案(考慮所有的GUI類庫),以至於權衡選擇哪種解決方案就成了艱巨的工作。C++的附屬復雜性是一個歷史遺留問題,C++0X做了多番努力試圖擺脫這個問題;關於設計的靈活性並不是一件壞事——如果你考慮到它可以幫助好的設計師做出好的設計;如果有人譴責它,因為這樣浪費了自己很多腦細胞,那這只是個人問題,而不是語言問題,或許這樣的人就不應該負責做設計。如果你擔心你的C++編程伙伴因為受這些高水平特征的誘惑而使得你的工程代碼一團遭,那麼,或許你應該建立一個編碼標准並強制執行它(或者你可以遵循the collective wisdom,或者堅持C規范,或者帶有C++類的C規范),而不是因為存在風險而退縮放棄(政策可以避免風險),因為如果不這樣做,你將再也不能接觸所有C++類庫。
另一方面,存在著最重要的心理上問題——如果一個語言中存在一個稀奇古怪的性質,那麼最終就會有人發現它,然後人們就會被它吸引,這就會吸引那些本來在努力做一些有用的事的人的精力(有點像墨菲法則),不要去打擾那些正在做完美解決方案的人。人們天生就會被一些稀有資源吸引。結論就是:訣竅和稀奇古怪的性質就是稀有資源,所以會引起人們的注意,更不必說掌握一種訣竅可以使人感覺自己與眾不同。糟糕的是,即使是沒有價值的訣竅也會引起人們的強烈注意。
C++裡有多少技巧?C++裡有多少訣竅?總之,C++有多少復雜之處?
公平地說,大多數竅門和技巧在最近幾年都已經被發現了(例如,modern C++),已經用在了真實需求中,特別是實現高靈活性和屬性類庫組件的需求(考慮所有在boost中的組件)。它們確實(在一些程度上說)引導了一些現實問題的完美解決方案。可以這樣考慮這件事情:如果你處在這樣一種情況下,你得使用竅門來實現一些確實有用的事情;或者你不使用竅門實現它,那麼其他人就不會從使用它上得到好處。你會選擇哪種?我想聰明的人會選擇前一種,不管竅門有多難,實現有多麻煩。
但是所有的爭論都不能改變這樣的事實:那就是我們值得擁有一種可以在代碼中干淨地表達我們的想法的語言。以boost.function/boost.bind/boost.tuple為例,variadic templates(可變模板)可以很容易地(通過減少通信線路為原來的十分之一)實現這三個(將來會更多)類庫,代碼會變得簡潔,盡可能地簡單。Auto, initializer-list, rvalue-reference, template-aliasing, strong-typed enums, delegating-constructors, constexpr, alignments, inheriting-constructors,等;所有這些C++0X的特性,它們都有一個共同的目標——去除語言中的各種附屬復雜性或阻礙。
就像Bjarne Stroustrup說的,顯然,C++太復雜;顯然,人們有些恐懼它而放棄它。但是“人們也需要相對復雜的語言來處理絕對復雜的問題”。我們不可能減少語言的特性來使語言變得更有力。像是模板這樣復雜的特性,或是更復雜的多重繼承,這些可能會對你的需求更有用,你只需要非常認真、必要地了解它們,這樣就不會搬起石頭砸到自己的腳。在C++的所有復雜性中,唯一妨礙我們的就是附屬復雜性(有人稱它為“阻礙”),它不是語言所支持的范例(只支持三個)。這就是我們為什麼要加強C++0X的重要原因,因為它的目標是去除C++中長期存在的附屬復雜性,使所有的訣竅變得融合(這種東西數量絕對很大;你可以參看有關C++的書籍或是C++根庫,你就會明白我在說什麼了),這樣我們才能清晰、直接地表達我們的想法。
結論
C++很難,難於正確使用。所以當你決定要用它的時候,一定要小心謹慎,一定要清楚你處在什麼位置,你真正想要什麼。下面是一個簡單的導引:
我們需要高效嗎?
如果需要,那麼
在我們的代碼中需要抽象嗎(這個問題一定要慎重考慮,因為很難估計使用C++的高水平特性所帶來的好處是否超過了正確使用它們的風險;正確的答案取決於你的編程水平訓練的有多好,你遵循什麼編碼標准,以及這種標准加強的有多好,等)?
如果需要,那麼就使用C++,否則,
我們需要C++類庫來減少我們的工作量嗎?
如果需要,那麼就使用C++,但同時要謹記你在做什麼——如果你的代碼並不真的需要所有夢幻抽象,那就不要勉強使用它們;不要僅僅因為你寫的代碼是.cpp,你使用了C++編譯器,就使用類或是模板。
否則,就使用C++,但是你可能會懷疑為什麼不用C++的C編碼核心。同樣的理由:人們很容易被稀奇古怪的語言特性所迷惑,即使他們真的不知道這些特性對他們是否有幫助——我可以不厭其煩地告訴你,我寫過一串類,只是為了找出“這些類到底是什麼鬼東西”。所以,如果你能堅持C核心或是帶有部分C++的C核心,並且保持事情簡單(KISS);或者如果你的代碼需要從C移植到C++,那麼就使用C++吧!但是一定要小心。另一方面,如果你的代碼既不需要高性能特性,也不需要C++類庫,因為你要做的事情很簡單,以至於你甚至不需要像是containers或是strings這樣方便的組件;或者你認為你的項目中使用C++能給你帶來的好處不足以值得你冒險;或者只是因為你沒有足夠能正確使用C++的人員,那麼你應該還是堅持用C。
最主要的:保持事情簡單(KISS)(但是要記住這種簡單可以通過使用高水平類庫來達到);必要的時候(即使是必要,也要少用;遵循好的設計原理,養成好的習慣)使用抽象。