[為了增加您冬天閱讀的樂趣,我們很榮幸地奉上Bjarne Stroustrup大神的這個包含3個部分的系列文章。第一部分在這裡;第二部分在這裡。今天我們正好在聖誕節之前完成這個系列。請欣賞。]
1. 簡介
本系列包括 3 篇文章,我將向大家展示並澄清關於C++的五個普遍的誤解:
- 1. “要理解C++,你必須先學習C”
- 2. “C++是一門物件導向的語言”
- 3. “為了軟體可靠性,你需要垃圾回收”
- 4. “為了效率,你必須編寫底層程式碼”
- 5. “C++只適用於大型、複雜的程式”
如果你深信上述誤解中的任何一個,或者有同事深信不疑,那麼這篇短文正是為你而寫。對某些人,某些任務,在某些時間,其中一些誤解曾經只是正確的。然而,在如今的C++,應用廣泛使用的最先進的ISO C++ 2011編譯器和工具,它們只是誤解。
我認為這些誤解是“普遍的”,是因為我經常聽到。偶爾,它們有原因來支援,但是它們經常地被作為明顯的、不需要理由的支援地表達出來。有時,它們成為某些場景下不考慮使用C++的理由。
每一個誤解,都需要一大篇文章,甚至一本書來澄清,但是這裡我的目標很簡單,就是丟擲問題,並簡明地陳述我的原因。
前三個誤解在我的前兩篇文章中呈現。
5. 誤解4:“為了效率,你必須編寫底層程式碼”
許多人相信高效率的程式碼必須是底層程式碼。一些人甚至認為底層程式碼天生就是高效的(“如果程式碼很醜陋,那它一定很高效!一定有人花費了大量時間和精力來優化它!”)。當然,你僅僅使用底層程式碼是可以寫出高效程式碼的,並且有時在直接處理機器資源時必須使用底層程式碼。然而,你一定要衡量一下你的工作是否有價值:現代C++編譯器非常高效,而現代機器架構非常複雜。如果必須使用底層程式碼,一定要通過介面封裝起來,以便於使用。通常,通過高層介面封裝底層程式碼,會帶來更好的優化(如,避免“濫用”底層程式碼)。在關注效率的場合,首先嚐試用高層抽象來呈現需要的解決方案,而不要不加考慮地使用位元位和指標。
5.1 C語言的qsort()
考慮一個簡單的例子。如果你需要對一組浮點數執行降序排序,你可以寫一段程式碼來實現。然而,除非你有極端特殊的需求(如,有記憶體容納不下的大量資料),這樣做就太天真了。數十年來,我們一直有效能可接受的排序演算法庫。我最不喜歡的就是ISO標準C的qsort()演算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int greater(const void* p, const void* q) // three-way compare { double x = *(double*)p; // get the double value stored at the address p double y = *(double*)q; if (x>y) return 1; if (x<y) return -1; return 0; } void do_my_sort(double* p, unsigned int n) { qsort(p,n,sizeof(*p),greater); } int main() { double a[500000]; // ... fill a ... do_my_sort(a,sizeof(a)/sizeof(*a)); // pass pointer and number of elements // ... } |
如果你不是一個C程式設計師,或者你沒有使用過qsort,那麼需要解釋一下;qsort接收4個引數:
- 指向順序儲存位元組的指標
- 資料元素個數
- 每個資料元素的位元組數
- 一個比較函式,引數是指向資料元素首個位元組的指標
注意,這個介面丟失了資訊。我們真正地目的不是對位元組資料排序。我們要對浮點數排序,但是qsort並不知道,因此我們不得不提供如何比較浮點數以及浮點數佔用的位元組個數等資訊。當然,如果編譯器知道這些資訊就更好了。然而,qsort的低層次介面阻止了編譯器使用型別資訊。不得不顯式地宣告簡單資訊也會增加出現錯誤的機會。qsort()函式的兩個整數引數的順序寫錯了嗎?如果我寫錯了,編譯器不會注意到。我的compare()函式的返回值是否遵循了C的3路比較的預設約定呢?
如果你閱讀了qsort()函式的一個工業強度實現(請閱讀一下),你會發現它努力地去彌補缺少的資訊。例如,用交換一定數量位元組的方式,來取代更有效率的浮點數交換。間接地呼叫比較函式也很耗時,除非編譯器使用常量方式傳遞指標。
5.2 C++’s sort()
比較一下qsort()的C++等價實現,sort():
1 2 3 4 5 6 7 8 9 10 11 12 |
void do_my_sort(vector<double>& v) { sort(v,[](double x, double y) { return x>y; }); // sort v in decreasing order } int main() { vector<double> vd; // ... fill vd ... do_my_sort(v); // ... } |
這裡不需要太多解釋。vector知道它的長度,因此我們不需要再顯式地傳遞元素個數了。我們不會“丟失”元素型別資訊,因此也不需要處理元素的位元組數。預設地,sort()以升序排序,因此我需要指定比較條件,就像在qsort()中做的一樣。這裡,我傳遞了一個使用>符號比較浮點數的lambda表示式。通常,這個lambda表示式會被我所知道的所有C++編譯器內鏈編譯,因此實際上比較操作變成了一個greater-than的機器指令;這裡沒有(低效的)間接函式呼叫。
我使用了sort()的容器版本,以避免顯式地使用迭代器。即,避免像下面這樣寫:
1 |
std::sort(v.begin(),v.end(),[](double x, double y) { return x>y; }); |
我也可以更進一步,使用C++14的比較物件:
1 |
sort(v,greater<>()); // sort v in decreasing order |
哪個版本更快呢?你可以不使用任何效能優化指令,編譯C版本的qsort()和C++版本,因此這是一個真正的程式設計風格的比較,而不是語言的比較。標準庫實現似乎一直使用與sort和qsort相同的演算法,因此這是一個程式設計風格的比較,而不是演算法的比較。當然,不同的編譯器和庫實現會給出不同的結果,但是對於每種實現,我們對不同層次抽象的結果有一個合理的認識。
最近我執行了這個例子,並且發現sort()版本比qsort()版本快2.5倍。你會因為編譯器和機器的差別,得到不同的結果,但是我從來沒看到qsort打敗過sort。我甚至看到過sort比qsort快10倍。為什麼呢?很明顯C++標準庫sort相比qsort,是一個更高層次的抽象,更通用和靈活。它型別安全,並使儲存型別,元素型別和排序演算法引數化。它裡面看不到指標,型別轉換,長度,或者位元組。C++標準庫STL,包括sort,努力地嘗試不丟失任何資訊,從而得到了良好的內鏈和優化效果。
普適性和高層程式碼能夠擊敗低層程式碼。當然不是一直這樣,但是sort/qsort的比較不是一個孤立案例。總是從高層,精準和型別安全的版本開始解決方案。(僅當)如果需要時則優化。
6. 誤解5:“C++只適用於大型、複雜的程式”
C++是一個巨型的語言。它定義的大小和C#與Java差不多。但是這並不意味著你必須知道每一個細節,或者在每一個程式中都用到所有特性。考慮一個只使用標準庫基本元件的例子:
1 2 3 4 5 6 7 8 9 10 |
set<string> get_addresses(istream& is) { set<string> addr; regex pat { R"((\w+([.-]\w+)*)@(\w+([.-]\w+)*))"}; // email address pattern smatch m; for (string s; getline(is,s); ) // read a line if (regex_search(s, m, pat)) // look for the pattern addr.insert(m[0]); // save address in set return addr; } |
我假設你知道正規表示式。如果不知道,現在或許是一個閱讀它的好時機。注意,我使用move語法來對返回潛在地大量字串進行簡化和提升效率(譯者注:move語法在本系列第二篇講解)。所有的標準庫容器都支援move構造方法,因此這裡不需要使用new。
為了能正常工作,我需要引用適當的標準庫元件:
1 2 3 4 5 6 |
#include<string> #include<set> #include<iostream> #include<sstream> #include<regex> using namespace std; |
測試一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
istringstream test { // a stream initialized to a sting containing some addresses "asasasa\n" "bs@foo.com\n" "ms@foo.bar.com$aaa\n" "ms@foo.bar.com aaa\n" "asdf bs.ms@x\n" "\(bs.ms@x\)goo\n" "cft foo-bar.ff@ss-tt.vv@yy asas" "qwert\n" }; int main() { auto addr = get_addresses(test); // get the email addresses for (auto& s : addr) // write out the addresses cout << s << '\n'; } |
這僅僅是一個例子。可以很容易地修改get_addresses(),把regex模式作為引數,從而它能夠找到URL或者其他任何東西。也可以很容易地修改get_addresses(),使在每行文字識別超過一個模式。雖然C++是為靈活性和通用性而設計的,但不是每個程式都是一個完整的庫,或者應用程式框架。然而,這裡的關鍵點是,從流資料中提取郵件地址任務能夠被簡單地實現,並很容易測試。
6.1 庫
對任何語言,只使用語言內建特性(如if,for和+)編寫程式是相當乏味的。通常,會給出適當的庫(如圖形,路線規劃和資料庫),可以讓幾乎所有的任務都能夠在合理的工作量內完成。
ISO C++標準庫相對小一些(相對於商業庫),但是“就在那裡”,有大量的開源和商業庫。例如,利用(開源或有版權的)庫,如Boost[3],POCO[2],AMP[4],TBB[5],Cinder[6],vxWidgets[7],和CGAL[8],很多通用和專業的任務變得簡單。作為例子,讓我們修改上面的程式,從網頁內讀取URL。首先,我們改變get_addresses()來查詢符合模式的任意字串:
1 2 3 4 5 6 7 8 9 |
set<string> get_strings(istream& is, regex pat) { set<string> res; smatch m; for (string s; getline(is,s); ) // read a line if (regex_search(s, m, pat)) res.insert(m[0]); // save match in set return res; } |
這很簡單。接下來,我們需要考慮如何登入到網頁並讀取檔案。Boost有一個庫,asio,可以與網頁通訊:
1 |
#include “boost/asio.hpp” // get boost.asio |
需要連線到web伺服器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int main() try { string server = "www.stroustrup.com"; boost::asio::ip::tcp::iostream s {server,"http"}; // make a connection connect_to_file(s,server,"C++.html"); // check and open file regex pat {R"((http://)?www([./#\+-]\w*)+)"}; // URL for (auto x : get_strings(s,pat)) // look for URLs cout << x << '\n'; } catch (std::exception& e) { std::cout << "Exception: " << e.what() << "\n"; return 1; } |
檢視www.stroustrup.com網站上的C++.html檔案,內容如下:
1 2 3 4 5 |
http://www-h.eng.cam.ac.uk/help/tpl/languages/C++.html http://www.accu.org http://www.artima.co/cppsource http://www.boost.org ... |
我使用了set型別,因此URL會以字母順序列印出來。
我在一個函式(connect_to_file())中偷偷地“隱藏”了檢測和HTTP連線管理,這並非不切實際:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void connect_to_file(iostream& s, const string& server, const string& file) // open a connection to server and open an attach file to s // skip headers { if (!s) throw runtime_error{"can't connect\n"}; // Request to read the file from the server: s << "GET " << "http://"+server+"/"+file << " HTTP/1.0\r\n"; s << "Host: " << server << "\r\n"; s << "Accept: */*\r\n"; s << "Connection: close\r\n\r\n"; // Check that the response is OK: string http_version; unsigned int status_code; s >> http_version >> status_code; string status_message; getline(s,status_message); if (!s || http_version.substr(0, 5) != "HTTP/") throw runtime_error{ "Invalid response\n" }; if (status_code!=200) throw runtime_error{ "Response returned with status code" }; // Discard the response headers, which are terminated by a blank line: string header; while (getline(s,header) && header!="\r") ; } |
由於這是最常見的,我沒有從頭開始。HTTP連線管理大部分是從Christopher Kohlhoff的asio文件[9]中複製來的。
6.2 Hello,World!
C++是一種編譯型語言,設計它的首要目標是在關注效能和可靠性的場合,提供良好、可維護的程式碼(如,基礎設施[10])。它不是有意在小型程式中,直接和解釋型或小型編譯“指令碼”語言競爭。的確,這類語言(如JavaScript,或Java)通常是用C++實現的。但是有很多隻有數十行或幾百行的很有用的C++程式。
這裡提供一個能簡單嘗試的“Hello,World”例子,而不是(只)關注一個庫聰明和先進的部分。簡單安裝一個最小化的庫,寫一個最多一頁的“Hello,World”例子,展示一個庫能夠做什麼。在某些時候我們都是菜鳥。這裡,我C++版本的“Hello,World”是:
1 2 3 4 5 6 |
#include<iostream> int main() { std::cout << "Hello, World\n"; } |
在展示ISO C++和標準庫的時候,我發現了更長、更復雜、更無趣的版本。
7. 誤解的多種“用途”
在現實中誤解有時是有基礎的。對每一個誤解,某些人都會有多次經驗或情形導致他們有理由相信。今天,我認為它們是完全錯誤的、被誤解的,實話實說。一個問題是,誤解通常是為了支撐一個目的——或者它們已經消失了。這5個誤解扮演著多個角色:
- 提供安慰:不需要改變;不需要嘗試變革。這樣感到很舒適。變化可能失敗,因此相信新事物不可行會更好。
- 在開始一個新專案時,這樣可以節省時間:如果你(你自己)知道C++是什麼,你不需要再花費時間學習新知識。你不需要嘗試新技術。你不需要衡量潛在的效能障礙。你不需要培訓新的開發者。
- 你可以不必學習C++:如果這些誤解是真的,你究竟為什麼要學習C++?
- 促進替換語言和技術:如果這些誤解是真的,那麼明顯需要替換。
但是這些誤解不是真的,因此理智地改進現狀,替換C++,或者避免現代C++程式設計風格,都不能依靠這些藉口。從舊的C++觀點來看(用熟悉的語言子集和技巧)可能會舒服一點,但是軟體就是這樣,改變是必須的。我們可以比C,“帶類的C”,C++98等做的更好。
堅持曾經正確的看法,並不是沒有代價。相比現代的程式碼,它的維護程式碼更高。老的編譯器和工具集相比依賴現代結構化程式碼的現代工具,更加低效和難以分析。
現代C++(C++11,C++14)以及它支援的程式設計技術,與“常見的誤解”所代表的不同,並且比它要好的多。
如果你深信這些誤解之一,不要馬上就相信我的話,認為它是錯誤的。嘗試。測試。通過你關心的一些問題,衡量“老方法”和新的替換思路。嘗試真正地把握學習新工具和技術的時機,使用新方法寫程式碼的時機,應用現代程式碼的時機。不要忘記與堅持“老方法”比較可能的維護代價。澄清誤解的最佳方法是拿出證據。我在這裡只呈現例子和做出討論。
不,這不是一場“C++很完美”的討論。C++並不完美;它不是對每個人、每件事都最好的語言。其他語言也不是。接受C++現在的樣子,而不是20年前它的樣子,也不是某些人宣告它是什麼樣子。為了做出理性的選擇,拿出一些真正的資訊——只要時間允許——親自嘗試目前的C++如何處理你遇到的問題。
8. 總結
不要在沒有證據的情況下,相信C++的這些“常識”或使用它。這篇文章呈現了最頻繁表達的5個觀點,並逐一澄清,說明它們“僅僅是誤解”:
- 1. “要理解C++,你必須先學習C”
- 2. “C++是一門物件導向的語言”
- 3. “為了軟體可靠性,你需要垃圾回收”
- 4. “為了效率,你必須編寫底層程式碼”
- 5. “C++只適用於大型、複雜的程式”
它們是有害的。
9. 反饋
還有疑問?告訴我原因。你遇到過其他什麼誤解了嗎?為什麼他們相信誤解而不是實際經驗?你有哪些證據可能揭露一個誤解呢?
10. 參考文獻
- 1. ISO/IEC 14882:2011 Programming Language C++
- 2. POCO libraries: http://pocoproject.org/
- 3. Boost libraries: http://www.boost.org/
- 4. AMP: C++ Accelerated Massive Parallelism. http://msdn.microsoft.com/en-us/library/hh265137.aspx
- 5. TBB: Intel Threading Building Blocks. www.threadingbuildingblocks.org/
- 6. Cinder: A library for professional-quality creative coding. http://libcinder.org/
- 7. vxWidgets: A Cross-Platform GUI Library. www.wxwidgets.org
- 8. Cgal – Computational Geometry Algorithms Library. www.cgal.org
- 9. Christopher Kohlhoff : Boost.Asio documentation. http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio.html
- 10. B. Stroustrup: Software Development for Infrastructure. Computer, vol. 45, no. 1, pp. 47-58, Jan. 2012, doi:10.1109/MC.2011.353.
- 11. Bjarne Stroustrup: The C++ Programming Language (4th Edition). Addison-Wesley. ISBN 978-0321563842. May 2013.
- 12. Bjarne Stroustrup: A Tour of C++. Addison Wesley. ISBN 978-0321958310. September 2013.
- 13. B. Stroustrup: Programming: Principles and Practice using C++ (2nd edition). Addison-Wesley. ISBN 978-0321992789. May 2014.