GotW #16 Maximally Reusable Generic Containers
著者:Herb Sutter
翻译:kingofark
[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。
Revision 1.0
Guru of the Week 条款16:具有最大可复用性的通用Containers
难度:8 / 10
(你能让这个简单的container class具有多大的可适应性(flexibility)?预告:在本条款中,你将要学到的关于成员模板方面的知识绝不只是一点点而已。)
[问题]
为下面的定长(fixed-length)vector class实现拷贝构造(copy construction)操作和拷贝赋值(copy assignment)操作,以提供最大的可用性(usability)。提示:请考虑用户代码(client code)可能会用它做那些事情。
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];
};
[注意]
(1) 不要修改代码的其它部分。给出这个container的目的并不是要将其纳入STL——它至少有一个严重的问题;给出它的目的在于,我们能在一个简化了的情形下说明一些重要的问题。
(2) 这里的代码是根据最初由Kevlin Henney给出的例子改编而来的。Jon Jagger曾在
British C++ user magazine Overload栏目的第12和20期上面分析过那个最初的例子。(英国的读者注意了:在本期GotW中所给出的解答与
British C++ user magazine Overload第20期中的解答远不相同。事实上,在那一期中所提出的效率优化处理方案在我将给出的GotW解答中并不凑效。)
[解答]
[注意:这里的GotW原始解答存在一些bug,但已经在Exceptional C++和勘误表中解决了]
在本期GotW中,我们换一种方法来进行:由我来给出用于解答的代码,而由你来对代码进行解说。
提问:下面的解决方案具体是怎样运作的?为什么会这样运作?请对每一个构造函数(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.begin()+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];
};
现在,我们就来分析这段代码,看看它到底有没有圆满的回答本期的GotW问题。
[拷贝构造(Copy Contruction)操作和拷贝赋值(Copy Assignment)操作]
首先应该注意的是,本期条款的问题本身就有使用遮眼法(a red herring)的嫌疑——原始问题中给出的代码本身已经具有工作良好的拷贝构造函数(copy constructor)和拷贝赋值运算符(copy assignment operator)。而我们的解决方案意欲增加一个模板化的构造函数(templated constructor)和一个模板化的赋值运算符(templated assignment operator),以使得构造操作和赋值操作更具可适应性。
我要祝贺Valentin Bonnard以及其他一些人,是他们很快就指出,预想中的拷贝构造函数(copy constructor)其实压根儿就不是一个拷贝构造函数!事实上,连预想中的那个拷贝赋值运算符(copy assignment operator)其实也压根儿就不是一个拷贝赋值运算符!
其原因是:真正的拷贝构造函数或者拷贝赋值运算符只对完全相同类型的对象施以构造或赋值操作,并且,其如果是一个模板类的话,模板的参数也都必须完全相同。例如:
struct X {
template<typename T>
X( const T& ); // 这不是拷贝构造函数,因为T不会是X
template<typename T>
operator=( const T& );
//这不是拷贝赋值运算符,因为T不会是X
};
“但是,”你说,“这两个被模板化了的成员函数的确具有拷贝构造(Copy Contruction)操作和拷贝赋值(Copy Assignment)操作的准确形式呀!”这个嘛……告诉你吧:其实不然——根本不是这回事,因为在那两种情况下,T都不一定是X。下面是摘自CD2[注:同样的叙述也出现在1998年的官方标准中;“CD2”即“Committee Draft 2(委员会第2号草案)”]的叙述:
[12.8/2 note 4]
Because a template constructor is never a copy constructor, the presence of such a template does not suppress the implicit declaration of a copy constructor.
由于模板构造函数终究不是拷贝构造函数,因此这种模板的出现并不会隐藏原来隐含的拷贝构造函数之声明。
在[12.8/9 note 7]中也有关于拷贝赋值操作的类似叙述。如此一来,我们在解答中给出的代码实际上与原来问题中的原始代码有着相同的拷贝构造函数和拷贝赋值运算符——因为编译器始终生成它们隐含的版本。我们所做的改动只是增强了构造操作和赋值操作的可适应性,而不是替换掉了旧有的版本。举个例子来说:
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 = v; // 调用模板化了的赋值运算符
由此可以看出,本条款的问题所寻求的真正答案其实是提供了具有可适应性的“从其它fixed_vectors进行构造和拷贝的操作”,而不是具有可适应性的“拷贝构造操作和拷贝赋值操作”——它们早就存在了。
[构造操作和赋值操作的可用性]
我们增加的两个操作具有如下两个主要用途:
1. 支持可变的类型(包括继承在内)
尽管fixed_vector原则上应该保持在相同类型的container之间进行拷贝和赋值操作,但有时候从另一个包含不同类型的对象之fixed_vector进行构造和赋值操作,也是不无意义的。只要源对象可以被赋值给目的对象,就应该允许这种不同类型对象之间的赋值。例如,用户代码可能会这样使用fixed_vector:
fixed_vector<char,4> v;
fixed_vector<int,4> w(v); // 拷贝
w = v; // 赋值
class B { /*...*/ };
class D : public B { /*...*/ };
fixed_vector<D,4> x;
fixed_vector<B,4> y(x); // 拷贝
y = x; // 赋值
2. 支持可变的大小
与第1点类似,用户代码有时也可能希望从具有不同大小的fixed_vector进行构造或赋值。支持这个操作同样也是有意义的。例如:
fixed_vector<char,6> v;
fixed_vector<int,4> w(v); // 拷贝4个对象
w = v; // 对4个对象进行赋值
class B { /*...*/ };
class D : public B { /*...*/ };
fixed_vector<D,16> x;
fixed_vector<B,42> y(x); // 拷贝16个对象
y = x; // 对16个对象进行赋值
[另一种解决方案:标准库风格的解答]
我很喜爱以上函数的形式以及其甚佳的可用性,但它们还是有些做不到的事情。接下来,我们考察一种具有标准库风格的解答:
1. 拷贝(Copying)
template<Iter>
fixed_vector( Iter first, Iter last ) {
copy( first,
first+min(size,last-first),
begin() );
}
于是,当要进行拷贝操作时,我们不用:
fixed_vector<char,6> v;
fixed_vector<int,4> w(v); // 拷贝4个对象
而要用:
fixed_vector<char,6> v;
fixed_vector<int,4> w(v.begin(), v.end());
// 拷贝4个对象
对于一个构造操作而言,哪一种方案更好呢:是先前的预想方案,还是这个标准库风格的方案?前一个相对更容易使用一些,而后一个的可适应性更好(比如它允许用户选择操作范围并可以其它种类的container进行拷贝)——你可以选择任一种或者干脆两者兼而提供之。
2. 赋值(Assignment)
这里应注意的是,由于operator=()只能接收一个参数,因此我们无法让赋值操作把iterator的范围作为另一个参数。一个可行的方法是,提供一个具名函数(named function):
template<Iter>
fixed_vector<T,size>&
assign( Iter first, Iter last ) {
copy( first,
first+min(size,last-first),
begin() );
return *this;
}
于是,当要进行赋值操作时,我们不用:
w = v; // 对4个对象进行赋值
而要用:
w.assign(v.begin(), v.end());
// 对4个对象进行赋值
从技术上而言,assign()其实并不是必需的,没有它我们也可以达到相同程度的可适应性,但这样一来,进行赋值操作时就不太有效率,显得比较不爽:
w = fixed_vector<int,4>(v.begin(), v.end());
// 对4个对象进行赋值
对于一个赋值操作而言,哪一种方案更好呢:是先前的预想方案,还是这个标准库风格的方案?这一回我们就不能再像第1点中那样用其可适应性来衡量孰好孰坏了,因为用户可以很容易的编写拷贝操作的代码(这样甚至还更具可适应性)。用户将不会用:
w.assign(v.begin(), v.end());
而会直接用:
copy( v.begin(), v.end(), w.begin() );
在这种情况下,就没有必要编写assign()了。因此使用先前预想的方案或许更好一些,这样可以让用户代码在需要对某个范围内的对象进行赋值操作的时候直接使用copy()。
[为什么给出了缺省构造函数(default constructor)?]
最后的问题:既然第一种预想方案中的空的缺省构造函数之功能与编译器自己生成的缺省构造函数之功能相同,那为什么还要特意在解答代码中给出它来呢?这是因为,一旦你定义了一个任意形式的构造函数,编译器就不会为你生成其缺省版本了。显然,像上述那样的用户代码就需要这样做。
[小结:成员函数模板(Member Function Templates)到底怎么样?]
希望本期GotW条款已经使你确信,成员函数模板非常易用。另外,我还希望本期条款能够使你明白为什么成员函数模板会被广泛用于标准库中。如果你还对此用法不太熟悉,千万不要伤心欲绝——并不是所有现存的编译器都支持成员模板特性,只不过这是C++标准的规定,所有的编译器很快都将支持它。(在撰写本文时,Microsoft Visual C++ 5.0已经可以编译通过使用此特性的代码了,但在某些用户代码例程中,其编译器还是不能对osize参数进行推理分析。)
在你创建自己的classes时使用成员模板,不仅可以取悦用户,而且用户还会越来越多,并争先恐后的使用那些极易复用的代码。
(完)