条款20: 宁以pass-by-reference-to-const 替换pass-by-value
Prefer pass-by-reference-to-const to pass-by-value缺省条件下C++以by value方式传递对象至函数.函数参数以实际实参的副本为初值,而调用端所获得的亦是函数返回值的一个副本.这些副本由对象的copy构造函数产出,这可能使得pass-by-value成为昂贵的操作.考虑以下 class 继承体系:
class Person {
public:
Person();
virtual ~Persion();
...
private:
std::string name;
std::string address;
};
class Student : public Person {
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
现在考虑以下代码,其中调用函数validateStudent,后者需要一个Student实参(by value)并返回它是否有效:bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
当上述函数被调用时,发生什么事?Student的copy构造函数会被调用,以plato为蓝本将s初始化.当validateStudent返回s会被销毁.因此,对此函数而言,参数的传递成本是"一次Student copy构造函数调用,加上一次Student析构函数调用".
Student对象内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象.此外Student对象继承自Person对象,所以每次构造Student对象也必须构造出一个Person对象.一个Person对象又有两个string对象...,最终结果是,以by value方式传递Student赌侠ing会导致调用一次Student copy构造函数,一次Person copy构造函数,四次string copy构造函数.当函数内的那个Student副本被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作.因此,以by value方式传递一个Student对象,总体成本是"六次构造函数和六次析构函数"!
回避所有那些构造和析构动作的方法就是pass by reference-to-const:
bool validateStudent(const Student &s);
这种传递方式的效率高得多:没有任何构造函数和析构函数被调用,因为没有任何对象被创建.修订后的这个参数声明中的 const 是重要的.原先的validateStudent以by value方式接受一个Student参数,因此调用者知道它们受到保护,函数内绝不会对传入的Student作任何改变;validateStudent只能够对其副本做修改.现在Student以by reference方式传递,将它声明为 const 是必须要的,因为不这样做的话调用者会担心validateStudent会不会改变它们传入的那个Student.以by reference方式传递参数也可以避免slicing(对象切割)问题.当一个derived class 对象以by value方式传递并被视为一个base class 对象,base class 的copy构造函数会被调用,而"造成此对象的行为像个derived class对象"的那些特化性质全被切割,仅仅留下一个base class 对象.这不应该令人惊讶,因为正是base class 构造函数建立了它.但这绝不会想要的,因为这样就表现不出多态行为了.
解决切割(slicing)问题的办法,就是以by reference-to-const 的方式传递.
如果窥视C++编译器的底层会发现,reference往往以指针实现出来,因此pass by reference通常意味着真正传递的是指针.因此如果有个对象属于内置类型(例如 int),pass by value往往比pass by reference的效率高些.对内置类型而言,当有机会选择采用pass-by-value或pass-by-reference-to-const 时,选择pass-by-value并非没有道理.这个忠告也适用于STL的迭代器和函数对象,因为习惯上它们都被设计为passed by value.
一般而言,可以合理假设"pass-by-value并不昂贵"的唯一对象就是内置类型和STL的迭代器和函数对象.至于其他任何东西都要遵守本条款的忠告,尽量以pass-by-reference-to-const 替换pass-by-value.
注意:
尽量以pass-by-reference-to 替换pass-by-value.前者通常比较高效,并可避免切割问题(slicing problem).
以上规则并不适用于内置类型,以及STL的迭代器和函数对象,对它们而言,pass-by-value往往比较恰当.
条款21: 必须返回对象时,别妄想返回其reference
Don't try to return a reference when you must return an object一旦程序员领悟了pass-by-value的效率牵连层面,就会一心一意根除pass-by-value带来的种种罪恶.在坚定追求pass-by-reference的纯度中,会犯下一个致命错误:开始传递一些reference指向其实并不存在的对象.
考虑一个用以表现有理数(rational numbers)的 class,内含一个函数用来计算两个有理数的乘积:
class Rational {
public:
Rational(int numberator = 0, int denominator = 1);
...
private:
int n, d;
friend const Rational operator* (const Rational& lhs, const Rational& rhs);
};
条款3说明了为什么 friend 函数的返回类型是 const.这个版本的operator*系以by value方式返回其计算结果(一个对象).如果可以传递reference,就不需要付出构造和析构的代价.但是所谓reference只是个名称,代表某个既有对象.任何时候看到一个reference声明式,都应该立刻想一想,它的另一个名称是什么?因为它一定是某物的另一个名称.以上述operator*为例,如果它返回一个reference,后者一定指向某个既有的Rational对象,内含两个Rational对象的乘积.不能期望这样一个Rational对象在调用operator*之前就存在.也就是说,如果有:
Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5
Rational c = a * b; // c应该是3/10
期望"原本就存在一个值为3/10的Rational对象"并不合理.如果operator* 要返回一个reference指向此数值,它必须自己创建那个Rational对象.函数创建新对象的途径有二:在stack空间或在heap空间创建.如果定义一个local变量,就是在stack空间创建对象.根据这个策略试写operator* 如下:
const Rational& operator* (const Rational& lhs, const Rational& rhs) {
Rationla result(rhs.n * rhs.n, lhs.d * rhs.d); // warning!糟糕的代码
return result;
}
可以拒绝这种做法,因为目标是要避免调用构造函数,而result却必须像任何对象一样由构造函数构造起来.更严重的是:这个函数返回一个reference指向result,但result是个local对象,而local对象在函数退出前被销毁了.因此这个版本的operator*并未返回reference指向某个Rational,它返回的reference指向一个"从前的"Rational;一个旧时的Rational;一个曾经被当作Rational但如今已经被销毁了的.任何调用者甚至只是对此函数的返回值做任何一点点运用,都将坠入"无定义行为"的恶地.事实的真想是,任何函数如果返回一个reference指向某个local对象,都将一败涂地.(如果函数返回指针指向一个local对象,也是一样)于是考虑在heap内构造一个对象,并返回reference指向它.Heap-based对象由 new 创建,所以得写一个heap-based operator* 如下:
const Rational& operator* (const Rational& lhs, const Rational& rhs) {
Rational& result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // warning!更糟糕的写法
return *result;
}
还是必须付出一个"构造函数调用"代价,因为分配所得的内存将以一个适当的构造函数完成初始化动作.但又发现一个问题:谁应该对 new 出来的对象实施 delete?即使调用者诚实谨慎,并且出于良好意识,还是不太能够在这样合情合理的用法下阻止内存泄露:
Rational w, x, y, z;
w = x * y * z; // 与operator*(operator*(x, y), z)相同
这里,同一语句内调用了两次operator*,因而两次使用 new,也就需要两次 delete.但却没有合理的办法让operator* 使用者进行那些 delete 调用,因为没有合理的办法让它们取得operator* 返回的reference背后隐藏的那个指针.这绝对导致资源泄露.但或许注意到了,上述不论on-the-stack或on-the-heap做法,都因为对operator*返回的结果调用构造函数而受到惩罚.最初的目标是要避免如此的构造函数调用动作.或许下面这样的代码可以实现目标:
const Rational& operator* (const Rational& lhs, const Rational& rhs) {
static Rational result; // warnging!又是一堆烂代码
result = ...;
return result;
}
就像所有用上 static 对象的设计一样,这一个也立刻造成对多线程安全性的疑虑.一个"必须返回新对象"的函数的正确写法是:就让那个函数返回一个新对象.对Rational的operator* 而言意味着以下写法(或其他本质上等价的代码):
inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
当然需要承受operator* 返回值的构造成本和析构成本,然而长远来看那只是为了获得正确行为而付出的一个小小的代价.并且C++和所有编程语言一样,允许编译器实现者施行最优化,用以改善产出码的效率.因此,在某些情况下operator* 返回值的构造和析构可被安全地消除.因此当必须在"返回一个reference和返回一个object"之间抉择时,需要挑出行为正确的那个.
注意:
绝不要返回pointer和reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static 对象而有可能同时需要多个这样的对象.