Item 3:使容器里的对象拷贝正确且高效
scott meyers 著
刘未鹏 译
容器持有对象,但并非你原先给它的那个。此外,当你从容器中得到一个对象,这个对象并不是原先在容器中的那个(译注:你得到了一个拷贝)。当你调用insert,push_back等操作向容器中插入元素时,进入容器的只是对象的一个拷贝。当你调用front或back从容器取得元素时,你得到的也只是容器中原先那个对象的一个拷贝。拷贝进去(copy in),拷贝出来(copy out),那才是STL的方式。
一旦一个对象被置于了容器之中, 以后它被拷贝将不是什么稀奇事儿。如果你对vector,string,deque插入或擦除元素,典型地,元素会以拷贝的方式被搬移(条款5和条款14)。如果你使用排序算法(条款31):next_permutation,previous_permutation;remove,unique(或者它们的同类算法(条款32));rotate,reverse.etc.,对象将以拷贝的形式被搬移。是的拷贝对象是STL的方式。
或许你想知道拷贝是如何进行的。那很简单——对象用它的“具有拷贝能力”的成员函数来进行拷贝,尤其是它的拷贝构造函数(copy constructor)和拷贝赋值操作符(copy assignment operator)。对于一个用户自定义的类如Widget,这些函数传统上像这样定义:
class Widget{
public:
...
Widget(const Widget&); //copy ctor
Widget& operator=(const Widget&); //copy assignment operator
...
};
如果你不自己定义这些函数,你的编译器将会为你生成它们,而对内建型别(ints,pointers,etc.)的拷贝则是由“位逐次拷贝”来完成(关于拷贝构造函数和拷贝赋值操作符的细节,参见C++的介绍性书籍,在我的《Effective C++》的条款11和条款27集中讨论了这些函数的行为)。
因为有这些拷贝操作,本条款的动机现在已经很明显了——如果你用一个其拷贝动作十分昂贵的对象填满容器,“将对象放到容器中”这样简单的行为可以被证明是一个性能的瓶颈。你在容器内移动对象越频繁,内存的释放分配动作就越频繁。如果你有一个具有非传统意义上的拷贝动作的对象,则将它放到容器中几乎总是会带来一些不快的事情(条款8)。
当然,当存在继承时,拷贝会导致“切割”(slice)。就是说,如果你创建了一个容纳基类对象的容器,然后将派生类对象放进去——切割就产生了,对象的继承部分(即比基类对象多出来的部分)会经由基类对象的拷贝构造函数被切割掉。
vector<Widget> vw;
class SpecialWidget:public Widget //SpecialWidget inherits from
{...}; //Widget above
SpecialWidget sw;
vw.push_back(sw); //sw is copied as a base class object
//into vw .its specialness is lost
//during the copying
切割问题说明将派生类对象放入基类对象的容器几乎总是个错误。在这种情况下,如果你想要拷贝后的结果对象在行为上像个派生类对象(如:调用派生类的虚拟函数),则你总会得到错误的结果(更多关于切割问题的幕后信息请参见Effective C++条款22,本书的条款38有关于此问题的另一个例子)。
有一个能使拷贝高效,正确而且对切割问题“免疫”的简单方法,那就是在容器里存放指针(而不是对象)。也就是说,不去创建Widget的容器,而去创建Widget*的容器。拷贝指针是很快的,而且它总会以你期望的方式进行(对指针进行位拷贝)。拷贝一个指针时不会有切割现象发生(译注:因为指针从本质上只是一个存放地址的变量)。不幸的是,指针的容器也有与STL相关的令人头痛之处。你可以从条款7和33了解它们。当你试图寻求能够在避开效率,正确性,和切割问题的同时避免这种头痛的方法时,你会发现智能指针(smart pointer)是一个有吸引力的选择(条款7)。
如果所有这些听起来就像STL是copy-crazy的,请再想一想。是的,STL作了很多拷贝动作,但通常它是为避免不必要的拷贝而设计的。事实上,是为了避免不必要的对象构造。考虑使用C或C++的内建容器(array)的代码:
Widget w[maxNumWidget]; //create an array of maxNumWidgets
//Widgets,default-ctoring each one
以上的代码将构造maxNumWidget个对象,即使通常我们只打算使用其中的一部分,或者我们希望立即用我们从别处得来的信息(如:从文件读入)来改写被默认构造的对象。我们可以使用STL来替代array——我们可以使用vector,它只在需要的时候才增长:
vector<Widget> vw; //create a vector with zero Widget
//objects that will expand as needed
我们也可以创建一个保留了足够空间的空的vector,在其中没有Widget被真正构造(直到需要时):
vector<Widget> vw;
vw.reserve(maxNumWidgets); //see Item 14 for details on reserve
与array相比较,STL容器要“有礼貌”得多,它们只创建(通过拷贝)你要求它们创建的那么多对象。它们只在你指示它们的时候去做。只有当你明确指明它们应该使用默认构造函数的时候它们才会那么做。是的,STL容器拷贝对象,并且,你得理解它们这种行为的意义。但是请不要忘记它们只是array的高阶抽象。
本文地址:http://com.8s8s.com/it/it25550.htm