條款4~5:GotW#16 具有最大可複用的通用Containers
問題:
為下面的定長(fixed-length)vector class實現拷貝構造和拷貝賦值操作,以提供最大的可用性(usability)。提示:請考慮使用者程式碼可能會用它做哪些事情。
template<typename T,size_t size> class fixed_vector { public: typedef T* iterator; typedef const T* const_iterator; iterator begin(){return v_;} iterator end(){return v_+size;} const_iterator begin() const {return v_;} const_iterator end() const {return v_+size;} private: T v_[size]; };
注意:不要修改其他東西,這個容器並不打算遵守STL——它至少有個嚴重的問題。
解答:
下面的解決方案具體是怎樣運作的?為什麼這樣運作?請對每一個建構函式(constructor)和運算子(operator)都做說明:
template<typename T,size_t size> class fixed_vector { public: typedef T* iterator; typedef const T* const_iterator; fixed_vector(){} template<typename O,size_t osize> fixed_vector(const fixed_vector<O,osize>& other) { copy(other.begin(),other.begin() + min(size,osize), begin()); } template<typename O,size_t osize> fixed_vector<T,size>& operator = (const fixed_vector<O,osize>& other) { copy(other.begin(),other.end() + min(size,osize), begin()); return *this; } iterator begin(){return v_;} iterator end(){return v_+size;} const_iterator begin() const {return v_;} const_iterator end() const {return v_+size;} private: T v_[size]; };
現在分析這段程式碼:
【拷貝構造(copy constructor)和拷貝賦值(copy assignment)操作】
原始程式碼中給出了本身已經工作良好的的拷貝構造和拷貝賦值。而我們的解決方案中欲增加一個模板化的的建構函式和一個模板化的賦值運算子函式,以使得構造操作和賦值操作更具適應性。
但是這兩個函式壓根不是拷貝構造和拷貝賦值函式。
其原因:真正的拷貝構造和拷貝賦值只對完全相同的型別物件施以構造和賦值操作,並且如果是一個模板類的話,模板的引數也都必須完全相同。例如:
struct X { template<typename T> X(const T&); //不是拷貝構造,T不可能是X template<typename T> operator=(const T&);//不是拷貝賦值,T不可能是X };
雖然這兩個模板化了的成員函式的確具有拷貝構造和拷貝賦值操作的準確形式,其實不然——根本不是這回事,因為在那兩種情況下,T都不一定是X。
由於模板建構函式終究不是拷貝建構函式,因此這種模板的出現並不會隱藏原來的拷貝建構函式。
如此一來我們所做的改動只是增強了構造操作和賦值操作的可適應性,而不是替換掉隱藏的版本。舉例:
fixed_vector<char,4> v;
fixed_vector<int,4> w;
fixed_vector<int,4> w2(w); //呼叫隱藏的拷貝構造
fixed_vector<int,4> w3(v); //呼叫模板化的轉換建構函式
w = w2; //呼叫隱藏的拷貝賦值
w = v2; //呼叫模板化的轉換賦值函式
由此看出,本條款的問題所尋求的真正答案是提供了可適應性的“從其他fixed_vectors進行構造和拷貝的做操”,而不是具有可適應性的“拷貝構造和拷貝賦值操作”——它們早就存在了。
【構造操作和賦值操作的可用性】
我們增加的兩個操作具有如下兩個主要用途:
- 支援可變的型別(包括繼承在內)
儘管fixed_vector原則上應該保持在相同型別的container之間的拷貝和賦值操作,但有時候從另一個包含不同型別的物件至fixed_vector進行拷貝和賦值操作,也是不無意義的。只要源物件可以被賦值給目的物件,就應該允許這種不同型別物件之間的賦值。例如,使用者程式碼可能會這樣使用fixed_vector:
fixed_vector<char,4> v;
fixed_vector<int,4> w; //拷貝
w = v; //賦值
class B{/*...*/};
class D : public B{/*...*/};
fixed_vector<D,4> x;
fixed_vector<B,4> y(x); //拷貝
y = x; //賦值
- 支援可變的大小
與第一點類似,使用者程式碼有時候也希望從具體不同大小的fixed_vector進行構造和賦值。支援這樣的操作也是有意義的。例如:
fixed_vector<char,6> v;
fixed_vector<int,4> w(v); //拷貝四個物件
w = v; //對四個物件進行賦值
class B{/*...*/};
class D : public B{/*...*/};
fixed_vector<D,16> x;
fixed_vector<B,42> y(x); //拷貝16個物件
y = x; //對16個物件進行賦值
【另一種解決方案:標準庫風格的解答】
雖然上面函式形式及其更加的可能性,但它們還是有些做不到的事情。下面考察具有標準風格的解答:
- 拷貝(Copying)
template<class RAIter>
fixed_vector(RAIter first,RAIter last)
{
copy(first,first+min(size,(size_t)last-first),begin);
}
//拷貝就不寫成如下的形式
fixed_vector<char,6> v;
fixed_vector<int,4> w(v);
//而是應該寫成如下的形式
fixed_vector<char,6> v;
fixed_vector<int,4> w(v.begin(),v.end());
對於一個構造操作而言,哪一種方案更好呢:是先前的預想方案,還是這種標準庫風格的方案呢?前一種相對容易使用,而後一種更具可適應性(比如它允許使用者選擇操作範圍並可以其他種類的container進行拷貝)——可以任意選擇一種和都提供。
- 賦值(Assignment)
注意的是,由於operator=()只提供了一個引數,因此我們無法讓賦值操作把iterator的範圍作為另一個引數。一個可行的方法是,提供一個具名函式(name function):
template<class Iter>
fixed_vector<T,size_t>&
assign(Iter first,Iter last)
{
copy(first,last+min(size,last-first),begin());
return *this;
}
//不用這種賦值行銷
w = v;
//賦值操作正確形式
w.assign(v.begin(),v.end());
從技術上而言,assign()其實並不需要,沒有它我們一樣可以達到相同的可適應性。但這樣一樣,進行的賦值操作就不太有效率:
w = fixed_vector<int,4>(v.begin(),v.end());
對於賦值操作而言,用於很容易編寫拷貝操作的程式碼而不用寫assign()函式:
//不用寫這種形式
w.assign(v.begin(),v.end());
//可以用這種形式
copy(v.begin(),v.end(),w.begin());
這種情況下就沒有必要編寫assign()了,使用先前的預想方案更加好點,這樣可以讓使用者程式碼需要堆某個範圍內的物件進行賦值操作的時候直接使用copy()。
【為什麼給出了預設建構函式(default constructor)】
這是因為你一旦定義了任何形式的建構函式,編譯器就不會為你生成預設版本了。顯然,像上述程式碼就需要這樣做。
【總結:成員函式模板(Member Function Templates)到底怎樣】
希望本期的GotW條款已經使你確信,成員函式模板非常易用。這就是為什麼成員函式模板會被廣泛用於標準庫的原因。
在你建立自己的classes時使用成員模板,不僅可以取悅使用者,而且使用者還會越來越多,並爭先恐後的使用那些極易複用的程式碼。