C++之父Bjarne Stroustrup: 簡單的表述方式纔是最優的方案(圖靈訪談)

2016年的最後一天,圖靈訪談給各位小夥伴兒獻上特大彩蛋!借用Bjarne大師的話“趁你還足夠年輕的時候,喜歡上某些學科,選擇具有挑戰性和感興趣的工作並養成良好的習慣!”,預祝你們在2017年找到新的方向!

訪談嘉賓:

Bjarne Stroustrup(本賈尼·斯特勞斯特盧普)

1982年,貝爾實驗室(美國AT&T公司)的Bjarne Stroustrup博士在c語言的基礎上引入並擴充了面向對象的概念,發明了新的程序語言C++。之所以被命名爲C++,是爲了表達該語言與c語言的淵源關係。Bjarne Stroustrup博士因此被尊稱爲“C++語言之父”。

之後,面向對象的編程思想開始席捲整個開發領域,標準模板庫(STL)和微軟的VC++平臺推波助瀾,C++開始流行起來。可以說,C++對整個軟件開發及IT業的貢獻,不言而喻。

C++仍在它擅長的領域發揮着不可或缺的作用。作爲C++之父,Bjarne Stroustrup也一直致力於C++標準的改進和推廣,其著作《C++編程語言》《C++的設計和演化》和《C++加註參考手冊》等已成爲C++學習的經典讀物。

這裏寫圖片描述

訪談內容:

英文版

除了作爲編程技術大師爲人熟知以外,Bjarne還有很多至理名言被大家廣泛引用。在觀看之前的訪談時,我也被您發人深思和辯證的思維所折服。如何做到將自然語言和編程語言運用如此得體的高度呢?

我想要直接而簡潔地表達觀點,雖然並不總能成功,但這值得一試。記住,當你寫代碼的時候,並不僅僅是給編譯器看的。相反,代碼的“消費者”包括所有閱讀和維護代碼的人。如果你的代碼醜陋不堪、難以理解,它將無法運行甚至造成巨大的維護問題。因此,無論是代碼還是“普通文本”,其目的都是清晰地表達觀點,幫助其他人理解這些想法。寫作是一種縷清思路的方法——對自己和他人來講,都是。

我記得,您曾經討論過人們對C++的誤解(要理解C++,首先要學習C語言;C++是一種面向對象的語言;可靠的軟件需要垃圾回收機制;爲了提高效率,必須編寫低級代碼;C++只適合大型複雜的程序)現在,他們的偏見有所改觀嗎?

一些人瞭解了,還有很多人沒有。這些謬見普遍充斥於網絡上、文章和教科書裏,通常被理所當然地接受——即便沒有證據支撐仍然被當作事實陳述。所以,很難進行反駁。相信這些謬見的人並不認爲自己對C++持有偏見,他們認爲自己是進步的,甚至因爲這些觀點變得優越。

我想藉此機會,鼓勵大家花點兒時間(重新)審視下自己的觀點,同時簡單陳述下我自己的一些觀點。如果想從技術角度瞭解詳細的論證過程,請參考我的相關論文和書籍。

要理解C++,必須首先學習C語言。不是的,如果你本身已經是一名程序員了,完全可以直接進入類設計和使用各種庫。如果你剛開始接觸某種語言,能夠處理低級的編程問題,可以依賴C++的強類型檢查和各種庫更容易、更快速地掌握基礎知識。編程新手在使用低級工具(諸如指針、數組、malloc()或free()、cast、宏)的時候,總是會遇到麻煩,沒有理由讓他們遭受這些問題及複雜性。複製或比較C語言裏的字符串對於編程新手來說是痛苦、單調乏味的。

當然了,不瞭解指針、數組、自由存儲管理(動態內存管理、堆)等方面的知識,就不能在C++上有所建樹,但可以之後再學習,等掌握了編程常識和C++基礎以後再學習。基於這樣的想法,我爲大學新生(一年級學生)設計了一套課程並編寫了相應的課本:http://www.stroustrup.com/programming.html 。效果很好。

C++是一種面向對象的語言。不是的,對於大多數包含繼承性的傳統意義上的OO來說,不是這樣的。C++確實支持面向對象編程技術,也相當優秀,但這並不是C++的全部。現代C++,包括大部分的ISO C++標準庫,更多地不再遵循這種模式。C++開始使用簡單的具體類型和獨立函數,並且僅在應用程序域採用分層架構、需要運行時調用的時候才使用運行時多態性。大多數受歡迎的C++應用程序使用了很多技術,並不僅僅是傳統的面向對象技術,有時候甚至根本沒有采用面向對象技術。

可靠的軟件需要垃圾回收機制。不是的,GC有時會阻礙可靠性的達成。GC並不能消除所有的內存泄漏,不能解決非內存資源的管理問題。泄漏套接字、文件句柄、線程和鎖,可能比內存泄漏更容易讓系統停止。支持可靠性的最好辦法是,找到應對資源管理和錯誤處理的方法,比如C++提供的RAII (Resource Aquisition Is Initialization, 資源獲取即初始化)。我目前正在研究這種方法,爲資源安全和類型安全的C++提供一套全面的系統:Http://www.stroustrup.com/resource-model.pdf 。核心觀點是保證沒有泄漏,使垃圾收集器沒有必要存在。在不影響程序員用代碼簡單、直接地表達想法的前提下,保證沒有泄漏確實很難,但不是沒有可能。

爲了提高效率,必須編寫低級代碼。不是的,現代C++十分擅長低級優化和不同抽象層次間的優化,多少數量的代碼都無法跟這種能力相比,特別是現代架構具有深度緩存層次結構和配有大幅度指令調序的優化器的情況下。從更高的層面說,人類無法通過直接使用線程和鎖,得到最優化的結果,所以我們需要更高級的模型和算法,獲得正確性、可靠性、可預測性和原始性能。當關於機器、數據或算法的一些觀點被證明是毫無根據的時候,擺弄bit、byte和指針這些基礎會變得可悲。舉個列子,看一下我和別人合著的這篇文章(http://www.stroustrup.com/improving_garcia_stroustrup_2015.pdf .)。文章通過去掉精心設計的優化,提高了spec-mark程序的性能。最後生成的程序變得更精簡、更清潔、易於維護、可擴展,而且沒有副作用。關於零開銷抽象(zero-headed abstraction)的問題,我已經談了很多。最近,我還看到了很多負開銷抽象(negative-headed abstraction)的示例:通過簡化適當的抽象獲得最優化程序。

C++只適合大型複雜的程序。不是的,除非你認爲一兩頁代碼的量就算是大型、複雜的項目。要知道,任何有影響的程序都需要用到一個或更多個庫。這適用於每一種語言。不用任何庫,單憑光禿禿的語言,對於編程人員來講是痛苦的也是徒勞的。

在討論C++或是(更糟地)下定結論時,隨隨便便地堅持某種謬見,不加思考,只能說明懶惰。之前,我也寫了一篇澄清這些謬見的文章:www.stroustrup.com/Myths-final.pdf 。

**C++並非靜止不前的。標準委員會很快就將宣佈C++17的新增特徵。您認爲哪些特徵是值得期待的?** C++17新增了很多小的改進,對於每一個程序員來說都值得期待,但不要指望特別重大或是顛覆性的改進出現。預計在2017標準發佈後,這些新增特徵隨即可以在所有主要的編譯器裏應用。事實上,大多數C++17的特徵已經能用了。 新增特徵不一定對所有人有幫助,大多是爲了特定羣體的需要而完善C++或是標準庫的。你可以在搜索引擎裏輸入C++17找到新增特徵的詳細列表,不過,我會在這裏簡要談幾點我所喜歡的特徵: 1. 結構化綁定:使用C++17,我們可以打破結構,爲結構成員命名。例如:


     map<int,string>mymap;  
     //...  
     auto[iter,success]=mymap.insert(value);  
     if (success)f(*iter);  

對於map<int,string>insert()返回pair<mymap<int,string>::iterator,bool>,現在我們可以命名兩個返回值並直接使用,而不用創建一個pair對象,再訪問它的成員。

對於循環控制,這一點特別有用:

 for(const auto&[key,value]:mymap)  
      cout<<key<<”->”<<value<<’\n’;

我們用std::variant 讓union的顯式使用變得冗餘。現在,我們可以


     variant<int,double>v;       //可以是int或是double  
     v=12;  
     auto i=get<int>(v);         //i 變成了12  
     auto d=get<double>(v);      //會拋出bad_variant_access異常

2.對於之前沒有定義求值順序的情況,現在,多數情況下是可以定義的。例如

count<<f(x)<<””<<g(y)<<’\n’;

可以保證輸出g(y)的值之前先輸出f(x)的值。在C++17之前,f(x)g(y)是可以交錯的,這容易產生bug和混亂。

3.到2020年,我們將看到進行了重大改進的C++20。例如,

  • 概念——顯著簡化、更好指定的泛型編程
  • 模塊——更好的模塊化、更快的編譯
  • 協程——更加簡單、快速的生成器和pipeline
  • ——簡單、更快、更靈活的網絡
  • 新版STL——更快、更簡單、更靈活的算法和range

這並不是科幻小說裏的幻想,很多特徵已經開始在某些領域應用了。問題是,ISO C++標準委員會能否通過。

是否可以用某個新增的特徵爲例,向我們展示一下該特徵是如何符合C++的演化原則(直接硬件訪問;零開銷抽象;靜態類型)的?

提高硬件訪問能力和低級代碼的性能,是一項需要付出長期努力的任務。有些努力是看得見的,有些不容易看得到。

  • 我們一直在努力提高編譯時的計算能力,constexpr是這方面的典範。使用constexpr,我們可以指定一個函數在編譯時取值,如果用常量表達式作爲參數的話。同樣,我們也可以確保編譯時就完成某項計算工作。

     constexpr int isqrt(int n)     //對於常量參數,能在編譯時求值 
     {  
         int i=1;  
         while(i*i<n) ++i;  
         return i-(i*i!=n);  
     }  
     constexpr int s1=isqrt(9);    //s1是3  
     int x;                        //不是常量  
     //…  
     constexpr int s2=isqrt(x);    //編譯時,出錯  
     count<<weekday{jun/21/2016}<<’\n’; //星期二  
     static_assert(weekday{jun/21/2016}==tue);  

Constexpr配合使用const可以有效地提高性能,減少代碼大小,並有可能把數據直接嵌入代碼段中,或存儲在 ROM 中。因爲“你不能對常量創造某個競爭狀態”,所以有助於併發系統。

  • 另一個不太明顯的例子是,C++17確保多數情況下的複製省略。它讓我們可以從函數中方便地得到值。例如

     T compute(S a)
     {
         return complicated_computation_yielding_a_T(a);
     }
     T t=compute(s);

這裏沒有副本!能讓我們從指針和動態內存中解脫出來,因爲在現代的硬件訪問中,間接和動態內存越來越昂貴(相對地)。如果和之前講到的結構化綁定結合使用,會更有趣

     pair<T,T2>compute(S a,S2 b)
     {
         return{ comp1(a,b),comp2(a,b) };
     }

     auto[foo,bar]=compute(s,s2);

同樣,這裏不需要複製。

在過去的二十年裏,模板一直被認爲是零開銷抽象的,得到了迅猛的發展。它被廣泛複製於其它的編程語言中,但通常並不靈活,也不如C++模板運行時的效率。但是,模板基本上會提供編譯時的duck typing,而不是基於檢查接口的程序;它們會在之後的實例化階段進行類型檢查。因此,模板的迅猛發展導致了相當複雜的編程技術問題。我們需要讓泛型代碼更接近於非泛型代碼,更容易編寫,更易於編譯器檢查同時不影響或限制表達性。

Constexpr 函數的功能包括:不再需要模板就可以得到編譯時計算的值。如果你只需要某個類型的值,函數就能很好地表達。使用constexpr,編譯時函數就能像其它函數一樣,類型檢查也能像其它函數的一樣(不同於宏技巧或是傳統模板的元編程)。

“概念”是支持模板接口規範的語言特徵。遺憾的是,它沒能成爲C++17的新增特徵,但作爲ISO 技術規範已經應用於GCC6.2了。概念可以解決模板的很多問題。考慮一下標準庫函數advance()的簡化版,它允許迭代器向前移動n個元素。假如我們需要兩個版本,一個用於列表之類的東西,每次移動一個元素、操作n次;一個可以直接移動n個元素:

     template<Input_iterator Iter>
     void advance(Iter p,int n){while (n--)++p;}

     template<Random_access_iterator Iter>
     void advance(Iter p,int n){p+=n;}

也就是說,如果參數是一個隨機訪問的迭代器,使用第二種快速的版本;否則,使用第一個慢版本。

     void(vector<int>::iterator pv, list<string>::iterator pl)
     {
         advance(pv,17);          //fast
         advance(pl,17);          //slow
     }

這是優化後的快速方案,我用短短幾分鐘就能向新手解釋清楚。它跟“傳統模板編程”不同,在編寫方式和檢查方式上都不同。如果願意,我甚至可以進一步簡化advance的定義:

 void advance(Input_iterator p, int n){while(n--)++p;}
 void advance(Random_access_iterator p, int n){p+=n;}

這完全符合我們談論代碼的方式,任何一個新手也會相當合理地這樣認爲。

最近,我寫了一篇文章(www.stroustrup.com/good_concepts.pdf)詳細解釋了concept的觀點。

某種程度上講,C++對專家更友好,只有少數的專業人士才能很好地掌握C++。如何減少初學者的困難呢?

“只有少數的專業人士能夠很好地掌握C++”誇大了C++的難度,因爲確實有數以百萬的程序員用C++編寫出了優秀的系統。但坦白說,很多C++代碼並不符合專業質量的要求,我們還能做得更好。

C++讓編程專家很容易編寫出複雜、高性能、低資源消耗的代碼,但不足以成爲廣大普通程序員喜愛的語言,它需要簡化。

我努力說服ISO C++標準委員會的專家還有許多的編程教師,說明我們需要不斷的努力,開發和講授更簡單的方式,不能僅僅專注於最優化和最聰明的技巧。通常情況下,簡單的表述方式纔是最優化的方案,“聰明”的技巧對於讀者、維護人員、優化器來說可能是一種負擔。在談論用代碼表達思想的時候,我大多會用“聰明”表示“太複雜”。最好把聰明用在分析問題和找尋根本辦法上。

“用簡單的方案解決簡單的事情。”之前,C++98 標準模板庫採用for語句來控制循環執行:

 for(vector<int>::iterator p=v.begin();v!=v.end();++p)
     cout<<*p<<’\n’;

在C++11中,我們使用range-for-statement :

 for(auto x:v)
     cout<<x<<’\n’;

意思是“輸出 v 中的所有成員 x”。auto表示“讓 x 具有初始化器的類型,在這裏,也就是 v 的元素類型”。

語言特徵和標準庫中的組件並不能很好地處理複雜性問題。所以,我開始制定一些指導準則,幫助大家更好地使用C++。我的這一做法也得到了其他人的支持,目前我們正在一起開發名爲“C++核心準則”的項目,以及工具支持的相關問題。你可以到麻省理工開源項目下找到相關準則:https://github.com/isocpp/CppCoreGuidelines 。指導準則試圖幫助程序員識別那些次優、容易出錯的表達方式,最終編寫出可讀性強、易於維護、簡單高效、類型安全和資源安全的代碼(http://www.stroustrup.com/resource-model.pdf)。這並不是狂妄的想法!

這並不僅僅是爲編程專家設計的。無論是專家還是初學者,都應該瞭解運用工具支持檢測問題的觀點。工具支持的早期版本可以在Visual Studio,Clang tidy和其他地方找到。

事實上,我們制定的指導準則已經受到了很多中高級編程人員的歡迎,他們把指導準則當作閱讀材料,學習如何更加高效地使用C++11和C++14。每一個準則都有基本原理的支持,同時提供了正反面的代碼示例。

Guidelines Support Library的未來發展規劃是怎樣的?未來是否會像標準模板庫一樣,由主要的編譯器支持或是提供?

核心準則的目的是提供一種進一步利用C++的方式,可以用來回答“未來5年代碼是什麼樣”的問題。利用C++11和C++14已經能夠很好地編寫代碼了,但程序員個體忙於業務,沒有時間來評價新的工具,所以指導準則和工具支持就顯得很有必要。

具體的支持有兩種形式:

  • 彌補ISO 標準庫不足的GSL
  • 幫助執行準則、提供準確性保證的靜態分析工具

GSL很小(也就十幾種類型和函數),主要目的在於避免程序員直接使用C++當中最棘手、最不安全的部分。比如,C++裏有一種not_null 類型確保指針不是nullptr,和span類型把(pointer, size) pair傳遞給函數。

關於GSL在GCC、Clang和微軟的實現,可以參見GitHub上麻省理工學院下的開源項目:https://github.com/Microsoft/GSL 。考慮到兼容問題,我們正在努力實現GSL的標準樣式規範。

核心指導準則是ISO標準委員會部分成員和其他外部人士共同執行的項目,它不是標準委員的工作。雖然我們已經向標準大會申請並希望某些GSL進入標準庫,但現在它們還是各自獨立的。

相比較其他程序員,高技能的程序員具有哪些品質?接觸編程學習較早,更加刻苦努力……

保持好奇心,願意終身學習下去;面對困難時,堅持不妥協;不止在編程方面,設計和電腦方面的基礎知識也必須堅實;樂於和系統用戶進行有效的溝通。

編程學習沒有“最佳年齡”或“最晚年齡”之說。如果你沒有在10歲、20歲或是30歲的時候開始接觸編程,這並不會影響你成爲偉大編程大師的可能。我20歲纔開始編程!重要的不僅是成爲一名編程人員,你還要對自己設計的程序有感覺,你要了解相關學科、領域的知識經驗。我認識的一些優秀編程人員並不是計算機科學專業出身的:有學習數學的、工程的、歷史的、化學的、生物的,甚至還有哲學的。我認爲,真正重要的是,趁你還很年輕的時候,能夠喜歡上某些學科,選擇具有挑戰性和感興趣的工作並養成良好的習慣。

我並不認爲一味地刻苦努力,所有成績拿A就是正確的方法。許多優秀的程序員是非常全面的人才,遺憾的是,並不是所有的。

面對那些堅持“我不想知道如何彈鋼琴,只想知道如何像霍洛維茨一樣演奏”,急於尋求成功祕方的人,您的建議是?

霍洛維茨一生都在練習鋼琴演奏;如果你也想成爲編程界的霍洛維茨,就要決心用一輩子的時間去練習和學習。記住,“臺上一分鐘,臺下十年功。”霍洛維茨第一次公開演出之前,花了很多年的時間來練習。應該有15年的時間都在練習。

要成爲一名優秀的編程人員,你不需要是世界級的天才,也不用15年的編程學習,就可以開發出實際的應用程序。但我建議你,在把自己的編程成果展示給別人看之前,需要花時間認真地學習和練習編程。

我確信,霍洛維茨是從手指練習開始的,選擇專門爲初學者編寫或是簡單的曲目練習。他沒有一開始就選擇李斯特的《匈牙利狂想曲》,沒有人上來就選擇最難的曲目。我猜想,很少有人在缺乏堅實基礎知識的情況下能達到高水平的成就。可以奔跑,但要在學會中低水平的技能(走路)之後。


更多精彩,加入圖靈訪談微信!

這裏寫圖片描述

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