条款05:了解C++默默编写并调用哪些函数
Knowing what functions C++ silentlywrites and calls
一个 emptyclass(空类)什么时候将不再是 empty class(空类)?
答案是当 C++ 处理过它之后。如果你自己不声明一个拷贝构造函数,一个 copyassignment运算符和一个析构函数,编译器就会声明一个它自己的版本。此外,如果你没有构造函数,编译器就会为你声明一个缺省构造函数,所有这些函数都被声明为 public 和 inline。因此:
class Empty{};
在本质上和你这样写是一样的:
class Empty {
public:
Empty() { ... } // default constructor
Empty(const Empty& rhs) { ... } // copy constructor
~Empty() { ... } // destructor
Empty& operator=(const Empty& rhs) {... } // copy assignmentoperator
};
这些函数只有在它们被调用的时候编译器才会创建。下面的代码会促使每一个函数生成:
Empty e1; // default constructor & destructor
Empty e2(e1); // copy constructor
e2 = e1; // copy assignment operator
缺省构造函数和析构函数主要是给编译器一个地方放置 “幕后代码”的,如调用基类和非静态数据成员的构造函数和析构函数。注意,生成的析构函数是non-virtual的,除非它所在的类是从一个基类继承而来,而基类自己声明了一个 virtual destructor虚拟析构函数(这种情况下,函数的virtualness(虚拟性)来自基类)。对于拷贝构造函数和拷贝赋值运算符,编译器生成版本只是简单地将来源对象的每一个non-static成员对象拷贝至目标对象。
template<typename T>
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};
如果你已经为一个要求实参的类设计了构造函数,你就无需担心编译器会再添加一个default构造函数而遮掉你的版本。NamedObject 既没有声明拷贝构造函数也没有声明拷贝赋值运算符,所以编译器将生成这些函数(如果它们被调用的话):
NamedObject<int> no1("SmallestPrime Number", 2);
NamedObject<int> no2(no1); // calls copy constructor
编译器生成的拷贝构造函数会用 no1.nameValue 和 no1.objectValue 分别初始化 no2.nameValue 和 no2.objectValue。 nameValue 的类型是 string,标准 string 类型有一个拷贝构造函数,所以将以 no1.nameValue 作为参数调用 string 的拷贝构造函数初始化 no2.nameValue。而另一方面,NamedObject<int>::objectValue 的类型是 int(在这个模板实例化中 T 是 int),而 int 是 内置类型,所以将通过拷贝 no1.objectValue 的每一个bits初始化 no2.objectValue。编译器为 NamedObject<int> 生成的拷贝赋值运算符本质上与拷贝构造函数有同样的行为。
例如,假设 NamedObject 如下定义,nameValue 是一个 reference to string,而 objectValue 是一个const T:
template<class T>
class NamedObject {
public:
//以下构造函数不再接受const名称,因为nameValue如今是个//reference-to-non-const string
NamedObject(std::string& name, const T& value);
...// 如前,假设并未声明operator=
private:
std::string& nameValue; // 这里是reference
const T objectValue; // 这里是const
};
现在,考虑下面会发生什么:
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s; // 现在p的成员变量会发生什么?
注意nameValue ,C++ 并没有提供使一个reference引用指向另一个对象的方法。此时C++ 拒绝编译那一行赋值代码。如果你希望一个包含引用成员的类支持赋值赋值,你必须自己定义拷贝赋值运算符。对于含有const 成员的类,编译器会有类似的行为(就像本例中的 objectValue)。更改 const 成员是不合法的,所以编译器隐式生成的赋值函数无法确定该如何处理。
最后还有一种情况,如果基类的拷贝赋值运算符声明为 private,编译器拒绝为从它继承的派生类生成隐式拷贝赋值运算符。毕竟,编译器为派生类生成的拷贝赋值运算符想象中可以处理其 baseclass 成分,但它们当然无法调用那些派生类无权调用的成员函数。
· 编译器可以隐式生成一个 class(类)的 default constructor(缺省构造函数),copy constructor(拷贝构造函数),copy assignmentoperator(拷贝赋值运算符)和 destructor(析构函数)。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
Explicitly disallow the use ofcompiler-generated functions you do not want
房地产代理商出售房屋,服务于这样的代理商的软件系统自然要有一个类来表示被出售的房屋.每一件房产都是独特的,因此最好让类似这种企图拷贝 HomeForSale对象的行为不能通过编译:
class HomeForSale { ... };
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 企图拷贝h1,不该通过编译
h1 = h2; //企图拷贝h2,不该通过编译
阻止这一类代码的编译并非那么简单。因为如果你不声明拷贝构造函数和拷贝赋值运算符,而有人又想调用它们,编译器就会替你声明它们。另一方面,如果你声明了这些函数,你的类依然会支持拷贝,而我们此时的目的却是防止拷贝!
解决这个问题的关键是所有的编译器生成的函数都是 public的。为了防止生成这些函数,你必须自己声明它们,但是没有理由把它们声明为public的。相反,应该将拷贝构造函数和拷贝赋值运算符声明为private的。通过明确声明一个成员函数,可以防止编译器生成它自己的版本,而且将这个函数声明为 private的,可以成功防止别人调用它。
声明成员函数为 private 却故意不去实现它确实很好,在 C++ 的iostreams 库里,就有几个类用此方法防止拷贝。比如,标准库的实现中 ios_base,basic_ios 和 sentry,其拷贝构造函数和拷贝赋值运算符被声明为 private 而且没有被定义。
将这个窍门用到HomeForSale 上,很简单:
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); // 只有声明
HomeForSale& operator=(constHomeForSale&);
};
你会注意到,我省略了函数参数的名称。参数名称并非必要,只不过大家总是习惯写出来。毕竟,函数不会被实现,更少会被用到,有什么必要指定参数名称呢?有了上述类定义,编译器将阻止客户拷贝 HomeForSale objects对象的企图,如果你不小心在成员函数或友元函数中这样做,连接器会提出抗议。
将连接时错误提前到编译时间也是可行的(早发现错误毕竟比晚发现好)。在一个为 prevent防止拷贝而特意设计的基类中,声明拷贝构造函数和拷贝赋值操作符为private就可办到。这个基类本身非常简单:
class Uncopyable {
protected:
Uncopyable() {} // 允许derived对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); // 但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
为了阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
class HomeForSale: private Uncopyable {
... // class不再声明copy构造函数或copy赋值操作符
};
Uncopyable 的实现和使用包含一些微妙之处,比如,从 Uncopyable 继承不必是 public的,而且 Uncopyable 的析构函数不必是virtual的。因为 Uncopyable 不包含数据,所以它符合emptybase class optimization的条件,但因为它总是扮演基类,因此使用这项技术可能导致多重继承。通常,你可以忽略这些微妙之处,而且仅仅像此处演示的这样来使用 Uncopyable。你还可以使用在 Boost里的noncopyable类。
· 为了拒绝编译器自动提供的机能,将相应的member functions(成员函数)声明为 private,而且不要给出 implementations(实现)。使用一个类似 Uncopyable 的 base class(基类)是方法之一。