CUJ:標準庫:容納不完全型別的容器 (轉)
The Standard Librarian: Containers of Incomplete Types:namespace prefix = o ns = "urn:schemas--com::office" />
Matt Austern
http://www.cuj.com/experts/2002/austern.htm?topic=experts
--------------------------------------------------------------------------------
在1997年,C++標準完成前夕,標準化委員會收到了一個詢問:用不完全的型別建立標準容器是可能的嗎?委員會花了好一會兒才理解這個問題。這樣的事情意謂著什麼,究竟為什麼會想這樣做?委員會最終解決了問題並給予了回答。(僅僅為了讓你不必跳到最後面,回答是“No”。)但問題本身比回答更令人感興趣:它指出了一個有用的,未經廣泛討論的技巧。標準執行庫並不直接支援這個技巧,但兩者可以共存。
不完全的型別
我們都用過不完全的型別,以很熟悉的向前申明的形式[注1]。如果你申明瞭一個類T,你甚至能在它被定義之前以受一定限制的方式使用它。(更確切地說,是在它的定義體結束之前:一個類在它的定義裡面還是不完全的)。你能使用T*型別的指標和T&型別的引用;你能寫簽名中包含這樣的指標或引用的;你甚至能申明一個T型別的extern的。你不能做的是:不能使用指標的運算,不能定義T型別的變數,不能寫new T,不能從T進行繼承,也不能使用sizeof()。
這不是隨便列出來的。所有這些都是從這個事實推導的,如C++標準在一個腳註中指出的:“未完全定義的物件型別的大小和佈局是未知的。”如果一個型別是不完全的,你不能做任何需要知道它的大小或佈局的事情。你如何在不知道T型別的物件應該多大時建立這樣一個物件?宣告:
class T;
告訴T是一個clss或struct。(其反面,是說T是內建型別或其它class的別名。)這足以讓編譯器知道該如何處理T的指標或引用,但不足以建立一個T的物件:編譯器應該為這樣一個物件分配多少?它應該如何佈局其欄位?同樣地,指標標的運算是沒有意義的,因為p += 1這樣的要求編譯器知道p所指向的物件的大小。
為什麼你想擁有指向一個只被前置申明瞭的型別的指標?經典的例子是(Kernighan和Ritchie[注2]稱為“自引用結構self-referential structures”):一個類包含一個指向它自己的指標,象連結串列或樹的節點。舉例來說:
struct int_list_node {
int value;
int_list_node* next;
};
在類的定義體裡面,int_list_node自身是一個不完全型別:它一直是不完整的,直到編譯器看到了全部的定義體。如果next是int_list_node型別的,就了(一個類怎麼能包含它自己的一個例項?),但是指向int_list_node的指標或引用就沒問題。
自然,自引用結構不是不完全型別的唯一用處。如果你的設計中含有指向對方例項的多個類時,或含有互為友元的緊耦合類時,這是必須的:
class int_list;
struct int_list_node;
class int_list {
friend class int_list_node;
...
};
struct int_list_node {
friend class int_list;
...
};
這個例子說明了不完全型別的一個重要的方面:在一個某處是不完全的型別可以在稍後被完成。在此處,int_list在它的前向申明出現後是不完全型別,但在它的完整定義體之後是一個完全型別。
最後,前向申明可以被用作資料隱藏的一個技巧,以將實現與介面分離[注3]。你可以在標頭檔案中提供一個“不透明型別”my_class的前向申明,然後申明一個函式介面以提供所需要的操作。(你可以選擇在別處暴露完整的類定義體,或者也許完全不暴露)。自然地,標頭檔案中的函式的模樣有些限制。你可以寫:
my_class& clone(const my_class&);
但不可以寫:
int illegal_function(my_class);
或:
my_class illegal_function();
你不能以值傳遞的方式傳入或返回不完全型別,就象不能定義一個不完全型別的變數一樣。當然,限制同樣加在成員函式上,就象加在獨立函式上的一樣。正如你不能寫上面的illegal_function(),你也不能寫:
struct illegal_class {
my_class f();
};
不完全型別和模板
理解不完全型別對模板的影響,最好從這個法則出發:當你看到一個類别範本X
template
struct list_node {
T value;
list_node
};
不完全型別是list_node
template
struct ptr_list_node {
T* ptr_value;
ptr_list_node
};
class my_class;
ptr_list_node
用類my_class例項化ptr_list_node是合法的,即使我們擁有的只是my_class的一個前向申明;擁有ptr_list_node
然而,聯合使用前向申明和模板確實引入了在非模板類中沒有的新問題。
比如,考慮這個類:
template
struct X {
T f() {
T tmp;
return tmp;
}
};
這看起來非常象上面的illegal_class的例子。我們有一個成員函式f(),它返回一個T型別的值,並且我們有一個T型別的區域性變數。明顯,這是在T為不完全型別時不能做的,所以你可能認為如果只有my_class的前向申明的話,寫X
這是一個技術問題,但很簡單:一個函式模板不會進行錯誤檢查(除了瑣細的語法錯誤),除非它被例項化,並且成員函式除非被使用否則不被例項化。X
當然,這不是個非常令人感興趣的例子:X
template
struct X {
T f() {
T tmp;
return tmp;
}
};
class my_class;
X
class my_class {
...
};
你為什麼會期望這樣的定義鏈?這有一個重要的理由:它能讓你在my_class自己的定義體內部使用X
我們接近了現實中的事情。實踐中,你當然可能不必為前向申明煩惱:你可以立即定義my_class並在其中使用 X
class ComplexFloat :
public FieldCategory
{
...
};
基類封裝了所有數學域模型共用的東西。基類和派生類是相互依賴的:FieldCategory需要從ComplexFloat獲得operator*=()這樣的函式,然後,它將pow()和repeat()之類的函式提供給ComplexFloat。
標準容器
我們已經偏離原來的問題了。我們已經談論了不完全型別與模板,但沒有提到標準容器。標準並沒有以“curiously recurring template pattern”的形式定義它們,而這個技巧正是以這種形式出現的[注6]。於是,不完全型別的容器是從哪來的呢?
我們已經看到幾種形式的透過前置申明得到的近乎迴圈的東西,但還有一種我們還沒看到。int_list_node這樣的類含義一個指標指向另外一個int_list_node,但這不是非常具有柔性。首先,我們可能想擁有一個節點指向另外N個節點而不是隻一個。(許多應用包含樹狀結構,其一個節點可能有任意個子節點--比如,考慮一下XML。)其次,指標語義可能不很方便[注7](WQ注,標準STL容器總是值語義的,以避免麻煩的所有權與生命期問題)。明顯,我們不能定義一個類X,它包含一個X的物件陣列--就算我們可以,陣列也不能可變大小。但我們可能這樣代替嗎?
struct tree_node {
int value;
std::vector
};
從外部表現看,很像這個類的每個物件包含N個相同的其它例項。這是故意的:vector這樣的STL容器接近於內建的陣列。一個節點的第i個子成員就是n.children[i],並且因為子成員是tree_node物件而不僅是指標。我們能只用一行就複製整個子樹:
tree_node n2 = n1;
不用擔心記憶體約定或顯式的深複製。它看起來是迴圈的,但迴圈的表象並不必然違法;正如我們已經看到的,不是看起來迴圈的東西都真的是迴圈的。所有必需的是:定義一個vector
當標準化委員會最初認識到這是一個未決問題時,tree_node的例子是我試的第一個測試。我不知道該期望什麼;我當然知道實現這個特別的std::vector的版本的人(我)從沒想過這樣的可能性。令我吃驚的是,它能工作!立即,我們開始考慮更可能發生的程式--比如,Greg Colvin,用於實現有限元狀態機,其每個state物件包含一個std::map
struct state {
int value;
std::map
};
唉,狀態機是第一個表明這個問題不象我們所期望得那樣簡單的徵兆。這個狀態機編譯失敗,並且,在片刻的思考之後,我們認識到不該費心嘗試的--應該顯然任何類似的東西都不能工作的。然後,隨著更多的測試,我們發現,即使是tree_node這樣的例子都不能工作在所有STL實作上。最終,怎麼看都是太暗淡太難以接受;標準化委員會認為沒有任何其它選擇,除了說STL容器不能與不完全型別合作。額外地,我們也申請了對標準執行庫的其餘部分也禁止這麼做。在T或Char還沒被定義時,擁有std::complex
C++標準[注8]說不允許用一個不完全型別例項化標準執行庫中的模板:“後果未知……如果在例項化模板元件時不完全型別被作為實參(WQ注,附原文:the effects are undefined ... if an incomplete type is used as a template argument when instantiating a template component)”。某些實作允許在某些情況下這麼做,但那只是意外。(記住,“未定義行為”含蓋了所有可能--包括它可能如你所期望的那樣工作。)
回顧一下,在那個技術已經被更好地理解之後,那一個決定仍然看起來是基本正確的。是的,在某些情況下是可能實作一些標準容器以能用不完全型別例項化的--但同樣很清楚,在其它情況下這樣做很難或不可能。完全是運氣,我們所嘗試的第一個測試,使用std::vector,碰巧是容易的情況之一。
很容易明白,定義std::map
其它標準容器怎麼說,比如list或set?在這兒,我們進入了實現細節;很難明確證明例項化std::list
template
struct rb_tree_node {
V value;
rb_tree_node *parent, *left, *right;
bool color;
};
當然,問題是成員變數value:它意味著我們不能用不完全型別例項化rb_tree_node,於是也就意味著(如果set是按這種方式實現的,)我們不能用不完全型別例項化set。可能以其它方式實現set而繞過這個限制嗎?可能的。但,據我所知,還沒人嘗試過--恐怕以後也不會有人嘗試,因為繞開限制的可行方法會造成set變大或變慢或同時兩者。
對於vector,還有其它方法。C++標準沒有規定vector
template
class vector {
...
private:
Allocator a;
T* buffer;
typename Allocator::size_type buffer_size;
typename Allocator::size_type buffer_capacity;
};
這其中沒有任何東西要求T是完全型別;一個前向申明就足夠了。並且沒有明顯的變更(從Allocator進行繼承,使用三個指標而不是一個指標加兩個整數,等等)會影響這一點。的確,當為本文再次測試tree_node時,它在我試過的前三個編譯器上透過了[注9]
總結
我們處在何處?迴圈的tree_node這樣的設計對某些用途是非常好的,但如我們已經看到的,我們不能擁有它:它被C++標準明確禁止。但是這並不必然意謂著標準程式庫對這個設計是沒有用處的。重要的主意是表面上迴圈的設計(類X包含一個作為成員變數的容器,而此容器的value_type是X):它是在一個類自包含時的次好方案。C++標準說你不被允許使用任何標準容器類,但標準容器不是唯一的選擇。C++標準定義了容器的介面,而不只是一組不相干的類,而任何滿足那個介面的容器類都與list和set這樣的預定義類同樣好地適合執行庫的架構。
在C++的未來修訂版中,放鬆在用不完全型別例項化標準執行庫模板上的限制可能會有意義。很清楚,常規的禁令應該繼續存在--用不完全型別例項化模板是個麻煩的事情,而標準執行庫中太多的類對此沒有意義。但也許應該以個案方式被放鬆,而vector看起來正是這樣的特例的一個很好的候選者:它是一個標準容器類,而有很好理由用一個不完全型別例項化它,並且標準執行庫的實現者想讓它能工作。時至今日,事實上,實現者不得不故意禁止它!
注
[1] Actually, there are two other kinds of incomplete types: arrays whose size is unknown, and void, which behaves like an incomplete type that can't ever be completed. See ?.9, paragraph 6, of the C++ Standard. However, the most important kind of incomplete type is a class that has been declared but not yet defined; it's the only kind that I'll discuss.
[2] B. W. Kernighan and D. M. Ritchie. The C Programming Language, First Edition (Prentice-Hall, 1978). I meant it when I said that this was "the classic example"!
[3] This is a well-known technique for managing dependencies in large programs. See, for example, J. Lakos's Large Scale C++ Design (Addison-Wesley, 1996). One classic example of this technique is the familiar C stdio library.
[4] ?4.3.1, paragraph 2.
[5] J. J. Barton and L. R. Nackman. Scientific and Engineering C++ (Addison-Wesley, 1994.)
[6] J. O. Coplien. "A Curiously Recurring Template Pattern," February 1995, .
[7] See my column "The Standard Librarian: Containers of Pointers," C/C++ Users Journal Experts Forum,
[8] ?7.4.3.6, paragraph 2; this is the part of the Standard that discusses general requirements that the standard library places on user components.
[9] The three compilers I tried were g++ 2.95, Microsoft Visual C++ 7.0, and Borland C++ 5.5.1. Why did these results differ from the ones I got four years ago? I suspect it's because of changes in the compilers, not in the library implementations; some older compilers failed to obey the rule that an unused member function of a class template shouldn't be instantiated.
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993518/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- CUJ:標準庫:容納指標的容器 (轉)指標
- CUJ:標準庫:基於檔案的容器 (轉)
- CUJ:標準庫:標準庫中的搜尋演算法 (轉)演算法
- CUJ:標準庫:bitset和bit vector (轉)
- CUJ:高效使用標準庫:STL中的unary predicate (轉)
- CUJ:標準庫:定義iterator和const iterator (轉)
- CUJ:高效使用標準庫:set的iterator是mutable的還是immutable的? (轉)
- C++標準模板庫------容器C++
- 自動型別安全的.NET標準REST庫refit型別REST
- [C++][基礎]5_標準庫型別C++型別
- C++標準庫型別string用法小結C++型別
- 容器,型別轉換。List。型別
- 標準庫unsafe:帶你突破golang中的型別限制Golang型別
- STL標準模組庫:容器string模組
- java統一返回標準型別Java型別
- 標準模板庫STL (轉)
- c++標準程式庫:STL容器之mapC++
- SAP標準移動型別(Movement Type)型別
- 標準模板庫介紹(轉)
- [CUJ]泛型程式設計--轉移建構函式 (轉)泛型程式設計函式
- 標準HTML識別符號 (轉)HTML符號
- Python標準資料型別-數字Python資料型別
- STL 簡介,標準模板庫(轉)
- 關於不完全型別的認識型別
- 【轉載】JS Number型別數字位數及IEEE754標準JS型別
- 戴爾將Linux納入工業標準Linux
- 淺析騰訊雲伺服器標準型SA2與標準型S2的區別在哪裡?伺服器
- STL 簡介,標準模板庫[1] (轉)
- 主流資料庫欄位型別轉.Net型別的方法資料庫型別
- swift 存放多型別的容器Swift多型型別
- Python 優雅地 dumps 非標準型別Python型別
- 異常:標準表示式中資料型別不匹配資料型別
- CUJ:普及知識:typeint (轉)
- C 標準庫 -
- Golang 型別轉換庫 castGolang型別AST
- C++標準庫、C++標準模版庫介紹C++
- ORACLE標準版與企業版的差別(轉載)Oracle
- c/c++ 標準順序容器 容器的訪問,刪除 操作C++