富有活力的語言需要不斷改變和成長,C++也不例外。在本文中,Bjarne Stroustrup提出了自己對C++的設計和演化的看法。
<!-- frame contents -->
<!-- /frame contents -->
為了讓編譯器、工具和類庫實現者跟上節奏,讓用戶吸收標准C++所支持的編程技術,在早有預計的、沉寂了幾年之後,委員會再次考慮語言擴展問題了。"擴展工作組"已經建立了,它代替了"演化工作組"。名稱的改變(這是Tom Plum的建議)反映了更重要的是語言特性和標准類庫工具的集成。我仍然是該工作組的主席。我希望這可以確保C++版本的連貫性和最終結果的一致性。相似的,委員會成員資格也顯示了大量人員和組織的連續參與。幸運的是,也出現了很多新的面孔,為委員會帶來了新的影響和新的專家意見。
我們打算對語言本身的改變保持謹慎和保守,重點強調兼容性。主要的目的是把主要的努力引導到標准類庫的擴展上來。在標准類庫方面,我們的目標是大膽進取,利用一切機會。
對於標准類庫,我希望根據類庫技術報告的要素來建立它,使它成為一個用於系統編程的更廣泛的平台。例如,我希望看到用於某些領域的類庫,例如目錄/文件夾操作、線程和套接字。我還希望委員會同情很多新的C++程序員,提供類庫工具支持背景不同的新手(不是新程序員和C的難民)。例如,我希望看到一個使用范圍檢查STL的標准方法。我對最頻繁地被請求添加到標准類庫中的標准GUI(圖形用戶接口)的期望值很低。但是,奇跡有時候也會發生--記得STL嗎?
對於語言本身,我希望重點強調支持泛型編程的特性,因為泛型編程是語言的使用取得最大進步的領域。此處,我將調查兩個要害部分:
·概念(Concepts):用於模板參數的類型系統
·初始化器(Initializer)列表:初始化工具的泛化
與以往一樣,建議的數量仍然遠遠超出了委員會能夠處理和該語言能夠吸收的數量。請記住,接受所有好的建議是不可能辦到的。
該語言擴展以支持泛型編程的全部目標是為工具提供更大的一致性,答應我們用泛型直接表示用於解決問題的類。
我的其它優先考慮(與更好地支持泛型編程一起)是更好地支持初學者。目前的建議有一種值得注重的傾向,即這些建議照顧了哪些提出和評估建議的專家用戶。有些簡單地幫助那些新手的建議經常被忽略了。我認為這是一種潛在的致命的設計偏好。除非新手受到了充分的支持,否則只有很少人能夠成為專家。此外,很多人並不希望成為專家;他們希望仍然是"偶然的C++用戶"。例如使用C++進行物理計算或控制試驗設備的物理學家只有有限的學習編程技術的時間。計算機專家可能會在編程技術方面花費很多時間,而不僅僅是期望。我們必須消除那些采用優良技術的不必要的障礙。
一個非常簡單的例子如下:
vector<vector<double>> v;
在98年的C++中,這會導致語法錯誤,因為>>是一個單獨的詞匯記號,而不是封閉模板參數列表的兩個>。V正確的聲明可能是:
vector< vector<double> > v;
我把它看作是一種阻礙。我曾經建議這個問題值得解決,但是當前的規則和演化工作組用一些很好的理由兩次拒絕了我的建議。但是,這些理由都是語言技術方面的,而新手(包括其他語言的專家)沒有愛好。不接受第一種(也是十分)明顯的v聲明浪費了用戶和教師的時間。我希望>>問題和其它相似的"阻礙"不要再出現在C++0x中。實際上,我與Francis Glassborow和其他人一起,正在試圖系統地消除最頻繁發生的這類"阻礙"。 另一個"阻礙"是:使用默認的復制操作(構造或賦值)來復制帶有用戶自定義析構函數的類對象是合法的。在這種情況下,要求用戶自定義的復制操作將消除大量的、與資源治理相關的麻煩錯誤。例如,考慮下面這個過度簡單化的字符串類:
<!-- frame contents -->
<!-- /frame contents -->
class String {
public:
String(char* pp) :sz(strlen(pp)), p(new char[sz+1]) { strcpy(p,pp); }
~String() { delete[] p; }
char& operator[](int i) { return p[i]; }
private:
int sz;
char* p;
};
void f(char* x)
{
String s1(x);
String s2 = s1;
}
在構造s2之後,s1.p 和 s2.p指向相同的內存區域,而這塊內存被刪除了兩次,可能導致災難性的後果。這個問題對於經驗豐富的C++程序員來說是很明顯的,他們一般會提供適當的復制操作或禁止復制。但是,這個問題會嚴重地困擾新手,破壞其對語言的信任。
禁止帶有指針成員的類對象的默認復制行為可能更好,但是這會導致令人厭煩的兼容性問題。修補長期存在的問題的難度比表面看起來要復雜很多,非凡是在考慮C兼容性的時候。
1、概念(Concepts)
D&E(編者注:"C++的設計和演化"通常簡稱為D&E)關於模板的討論中包含的關於模板參數的約束問題就占用了整整三頁。很明顯,我覺得應該需要一個更好的解決方案。在使用模板(例如標准類庫的算法)的過程中出現的微小錯誤所導致的錯誤消息可能非常長,並且沒有對我們沒有任何幫助。這個問題是由於模板代碼絕對相信自己的模板參數。看看下面的find_if():
template<class In, class Pred>
In find_if(In first, In last, Pred pred)
{
while (first!=last && !pred(*first)) ++first;
return first;
}
在上面的代碼中,我們對In和Predicate類型作出了很多假設。從代碼中我們可以看出,不知什麼緣故,In必須用適當的語義支持!=、* 和++,並且我們必須能夠把In對象復制為參數和返回值。類似的,我們可以看到,我們可以調用一個Pred,其參數是從In返回的任何類型的*(取值操作符),並給結果應用了!操作符,這個結果可以被當作是布爾型的。但是,在代碼中所有的這些都是隱含的。標准類庫仔細地記載轉發迭代子(例子中的In)和謂詞(Pred)的這些需求,但是編譯器是不會閱讀手冊的。試試下面的錯誤,看你的編譯器顯示的錯誤信息:
find_if(1,5,3.14); // 錯誤
不完整的、但是十分高效的,以我的舊想法--讓構造函數檢查模板參數的假設條件--為基礎的解決方案現在已經廣泛使用了。例如:
template<class T> strUCt Forward_iterator {
static void constraints(T a) {
++a; a++; // 可以增加
T b = a; b = a; // 可以復制
*b = *a; // 可以廢棄和復制結果
}
Forward_iterator() { void (*p)(T) = constraints; }
};
上面的代碼定義了一個類,只有當T是一個轉發迭代子的時候,它才能編譯。但是,Forward_iterator對象沒有做任何實際的事務,因此編譯器只能(並且的確是)對這種對象做微乎其微的優化操作。我們可以在如下所示的定義中使用Forward_iterator:
template<class In, class Pred>
In find_if(In first, In last, Pred pred)
{
Forward_iterator<In>(); // 檢查模板參數類型
while (first!=last && !pred(*first)) ++first;
return first;
}
Alex Stepanov和Jeremy Siek做了很多工作來開發和普及這種技術。他們使用這種技術的一個地方是Boost類庫,但是目前你會在大多數標准類庫實現中發現約束類。在錯誤消息的質量方面,它們的差異是很大的。
但是約束類最多是一個不完整的解決方案。例如,在定義中進行測試--假如檢查工作只能在聲明中完成,那麼就會好很多。使用這種方式的時候,我們必須遵循接口的使用規則,並且可以開始考慮真正的模板分開編譯的可能性問題。
因此,讓我們告訴編譯器我們所期望的模板參數:
template<Forward_iterator In, Predicate Pred>
In find_if(In first, In last, Pred pred);
假設我們能夠表示出Forward_iterator和Predicate是什麼,那麼編譯器現在可以不理會它的定義,單獨地檢查find_if()調用了。這時我們所需要做的工作是為模板參數建立一個類型系統。在現代C++環境中,這種"類型的類型(types of types)"被稱為"概念(concepts)"。我們可以通過很多途徑來說明這種概念;從現在開始,把它們想作是直接受到語言支持的、擁有更好的語法的約束類。一個概念說明了某種類型必須提供的什麼工具,而不是說明它如何提供這些工具。完美的概念(例如<Forward_iterator In>)與數學抽象("對於所有的類型In,In可以被增加、銷毀和復制")非常類似,如同最初的<class T>就是數學上的"對於所有的類型T"。
只要給出了find_if()的這種聲明(並且不是定義)之後,我們就可以編寫
int x = find_if(1,2,Less_than<int>(7));
這個調用會失敗,因為int不支持*。換句話說,這個調用在編譯時會失敗,因為int不是一個Forward_iterator。重要的是,它使得編譯器輕易報告用戶語言中的錯誤,並且在編譯時,調用會被首先看到。
不幸的是,知道迭代子參數是Forward_iterator並且謂詞參數是Predicate也不足以保證find_if()調用成功編譯。這兩個參數是互相影響的。非凡是謂詞的參數是一個使用*(pred(*first))解除引用的迭代子。我們的目的是在與調用分離的情況下,完善模板的檢測,同時在不查看模板定義的情況下,完善每個調用的檢查,因此概念必須有充分的表現能力,能夠處理模板參數之中的這類迭代子。一種辦法是用平行的參數來表示概念,這與模板的參數化方式類似。例如:
template<Value_type T,
Forward_iterator<T> In, // 迭代子在T序列中
Predicate<bool,T> Pred> // 帶有 T 參數並返回一個布爾值
In find_if(In first, In last, Pred pred);
在上面的代碼中,我們要求Forward_iterator必須指向類型T的元素,它也是Predicate的參數類型。 通過普通參數(此處是參數T)來表達模板參數之間必要的關系,很不幸沒有強大的表現能力,導致我們添加模板參數,並且間接地(無法直接地)表達需求。例如,上面的例子不能說明把*first的結果作為參數傳遞給pred一定可行。
<!-- frame contents -->
<!-- /frame contents -->
其實,它說明的是Forward_iterator和Predicate共享了一個模板參數類型。為了處理這類問題,我們正在研究直接表達模板參數之間關系的可能性。例如:
template<Forward_iterator In, Predicate Pred>
where (assignable<In::value_type, Pred::argument_type>)
In find_if(In first, In last, Pred pred);
這種方法也有自己的問題,例如它的要求(where子句)趨向於增加模板定義本身的復雜性,並且流行的迭代子(例如int*)並不擁有成員類型(例如value_type)。
概念的一種可能的表達方式是直接支持我們過去使用的約束類這種表達方式。例如,我們采用如下的方式來定義前面例子中使用的Forward_iterator:
template <class T> concept Forward_iterator {
// 參數化的概念
Forward_iterator a;
++a; a++; // 可以增加
Forward_iterator b = a; b = a; // 可以復制
*b = *a; // 可以廢除和復制結果
T x = *a; *a = x; // 可以認為結果是T類型的
};
或者
concept Forward_iterator { // 概念沒有用參數表示
Forward_iterator a;
++a; a++; //可以增加
Forward_iterator b = a; b = a; //可以復制
*b = *a; // 可以廢除和復制結果
};
參數化的概念定義可用於find_if的第一種聲明,不帶參數的用於第二種。它們表現了可替換使用的語言設計。我們在這個領域還會提供一些設計選擇。但是,看看下面的情形:
int x = find_if(1,2,Less_than<int>(7));
這是不合格的,因為1和2是int型的,而int不支持*。假如我們使用參數化的概念設計,它也是不合格的,因為int不是一個能夠與Forward_iterator<T>匹配的參數化類型。另一方面,看下面的例子:
void f(vector<int>& v, int* p, int n)
{
vector<int>::iterator q = find_if(v.begin(),v.end(),Less_than<int>(7));
int* q2 = find_if(p,p+n,Less_than<int>(7));
// …
}
很明顯,我是在報告目前正在進行的工作,但是某種形式的概念成為C++0x的基石是很可能的。模板已經成為多數有效的(和高效的)C++編程樣式的要素,但是它遭受很多困擾:大量的、無用的錯誤消息,缺乏基於模板參數重載模板的工具,分開編譯很差。概念直接解決了所有這些問題,同時還沒有基於方法的抽象基類的主要缺陷--通過虛擬函數調用的運行時解析的性能開銷。重要的是,概念不依靠於顯式聲明的子類型層次,因此不需要邏輯冗余的層次關系,並且可以認為內建類型與類是平等的。
現在以概念和它與其它語言中相似的構造之間可能的關系為主題的論文很廣泛。Matt Austern、Jaako J?rvi、Mich Marcus、Gabriel Dos Reis、Jeremy Siek、Alex Stepanov和我都活躍在這個設計問題的領域。
2、泛化的初始化器 C++的一個基本的想法是"對用戶定義類型的支持如同內建類型一樣好"。但是,看看下面的情形:
double vd[ ] = { 1.2, 2.3, 3.4, 4.5, 5.6 };
vector<double> v(vd, vd+5);
我們可以直接使用初始化器列表來初始化該數組,然而對vector來說,我們做得最好(指壞處最少)的方式就是建立一個數組並用該數組來初始化vector。假如只有少量幾個初始化器值,我甚至於可能使用下面的方式來避免明確地說明初始化器值的數量(在上面的例子中是 5):
vector<double> v;
v.push_back(1.2);
v.push_back(2.3);
v.push_back(3.4);
v.push_back(4.5);
v.push_back(5.6);
我認為誰也無法適當地調用上面的任何解決方案。為了得到最輕易維護的代碼,並且不讓內建(並且是天生危險的)數組受到的"寵愛"比推薦的用戶定義類型多vector,我們可以編寫下面的代碼:
vector<double> v = { 1.2, 2.3, 3.4, 4.5, 5.6 };
或者
vector<double> v ({ 1.2, 2.3, 3.4, 4.5, 5.6 });
由於參數傳遞是在初始化過程中定義的,因此對於帶有vector的函數來說,這也是可行的:
void f(const vector<double>& r);
// …
f({ 1.2, 2.3, 3.4, 4.5, 5.6 });
我相信這種初始化器的泛化會成為C++0x的一部分。
它將成為構造函數檢查工作的一部分,因為人們發現的很多缺陷都似乎可以通過構造函數的泛化(例如轉發構造函數、有保障的編譯期構造函數、繼續的構造函數)來解決。