20.Prefer pass-by-reference-to-const to pass-by-value.(宁以pass-by-reference-to-const替换pass-by-value)
(1)缺省情况下C++以by value方式传递对象至函数。而调用端所获得的亦是函数返回值的一个复件。
1)考虑如下class继承体系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Person { public: Person(); 为求简化 ,省略参数 virtual ~Person(); ... private: std::string name; std::string address; }; class Student:public Person { public: Student(); ~Student(); ... private: std::string schoolName; std::string schoolAddress; }; |
2)接着考虑如下代码:
1 2 3 |
bool validateStudent(Student s); //函数以by value方式接受学生 Student plato; bool platoIsOk = validateStudent(plato);//调用函数 |
对此,Student的copy构造函数会被调用,以plato为蓝本将s初始化。并且,当validateStudent返回s会被销毁。因此,对此函数而言,参数的传递成本是“一次Student copy构造函数调用,加上一次Student析构函数调用”。
3)不仅如此,Student对象内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象。不仅如此,Student对象继承自Person对象,所以每次构造Student对象也必须构造出一个Person对象,一个Person对象又有两个string对象在其中,因此每一次Person构造动作又需承担两个string构造动作。
4)针对本例,以by value方式传递一个Student对象会导致调用一次Student copy构造函数、一次Person copy构造函数、四次string copy构造函数。当Student复件被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作。
5)因此,以by value方式传递一个Student对象,总成本是“六次构造函数和六次析构函数”。
(2)那么,有什么方法可以回避所有那些构造和析构动作呢?有的,那就是pass by reference-to-const:
bool validateStudent(const Student& s);
这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何对象被创建。
1)其中,参数声明中的const是重要的。因为不这么做的话,调用者会忧虑validateStudent会不会改变他们传入的那个Student。
2)以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割了,仅仅留下一个base class对象。
3)解决切割问题的办法,就是以by reference-to-const的方式传递。
(3)如果窥探C++编译器的底层,会发现,reference往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。因此如果你有个对象属于内置类型(例如int),pass by value往往比pass by reference-to-const的效率高些。
(4)一般而言,你可以认为“pass-by-value并不昂贵”的唯一对象就是内置类型和STL的迭代器和函数对象。至于其他任何东西都请遵守本条款的忠告,尽量以pass-by-reference-to-const替换pass-by-value。
请记住:
(1)尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
(2)以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
21.Don’t try to return a reference when you must return an object.(必须返回对象时,别妄想返回其reference)
学习了条款20之后,我们不能见到什么都想着传递reference。因为这里有一个致命的错误:传递一些reference指向其实并不存在的对象。
(1)考虑如下一个用以实现有理数的class,内含一个函数用来计算两个有理数的乘积:
1 2 3 4 5 6 7 8 |
class Rational { public: Rational(int numerator = 0, int denominator = 1); ... private: int n, d; 分子numerator和分母denominator friend const Rational operator*(const Rational& lhs, const Rational& rhs); }; |
1)这个版本的operator*以by value方式返回其计算结果(一个对象)。
2)对此,在学习了条款20之后,你可能会想,是否可以将上述函数返回reference?
我们来一点点分析:
(2)如果可以改而传递reference,就不需要付出额外代价。但是记住,所谓reference只是个名称,代表某个既有对象!任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。对上述operator*而言,如果它返回一个reference,那么它一定指向某个既有的Rational对象,内含两个Rational对象的乘积。
(3)因此,如果operator*要返回一个reference指向如此数值,它必须自己创建那个Rational对象。函数创建对象的途径有二:在stack空间或heap空间创建之。
1)如果定义一个local变量,就是在stack空间创建对象。因此有:
1 2 3 4 5 |
const Rational& operator*(const Rational& lhs, const Rational& rhs) { Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //警告:糟糕的代码! return result; } |
你可以拒绝这种做法,因为我们的目标是避免调用构造函数,而result却必须向任何对象一样地由构造函数构造起来。
更严重的是:这个函数返回一个reference指向result,但result是个local对象,而locak对象在函数退出前被销毁了。
2)于是,我们考虑在heap内构造一个对象,并返回reference指向它。Heap-based对象由new创建,所有有:
1 2 3 4 5 |
const Rational& operator* (const Rational& lhs, const Rational& rhs) { Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);//更糟糕的写法! return *result; } |
对此,你还是必须付出一个“构造函数调用”代价,因为分配所得的内存将以一个适当的构造函数完成初始化动作。
此外,又出现了一个问题:谁该对被你new出来的对象实施delete?
(4)上述不论on-the-stack或on-the-heap做法,都因为对operator*返回的结果调用构造函数而受惩罚。而我们最初的目标是要避免如此的构造函数调用动作。这个时候,你获取又会这样想了:使用static静态对象
1 2 3 4 5 6 |
const Rational& operator* (const Rational& lhs, const Rational& rhs) { static Rational result; //又一堆烂代码! result = ...; return result; } |
1)就像所有用上static对象的设计一样,这一个也立刻造成我们对多线程安全性的疑虑。不过这还只是显而易见的弱点。还有如下问题:
2)考虑如下客户代码:
1 2 3 4 5 6 7 8 9 |
bool operator==(const Rational& lhs, const Rational& rhs); Rational a, b, c, d; ... if ((a * b) == (c * d )) { ... } else { ... } |
会有什么问题?if语句里的表达式总是被核算为true,不论abcd的值是什么,多么可怕。一旦代码重写为等价的函数形式,很容易就可以了解出了是意外:
if (operator==(operator*(a,b), operator*(c, d) ) );
两次operator*调用的确各自改变了static Rational对象值,但由于它们返回的都是reference,因此调用端看到的永远是static Rational对象的“现值”。
(5)通过上述的种种分析,足够说服我们:令operator*这样的函数返回reference,只是浪费时间而已。
(6)综上,一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象呗!对Rational的operator*而言意味以下写法(或其他本质上等价的代码):
1 2 3 4 |
inline const Rational& operator* (const Rational& lhs, const Rational& rhs) { return Rational(lhs.n * rhs.n, lhs.d * rhs.d); } |
当然,我们需要承受operator*返回值的构造成本和析构成本,然而长远来看,那只是为了获得正确行为而付出的一个小小代价。
(7)我们把以上的讨论浓缩总结为:当你必须在“返回一个reference和返回一个object”之间抉择时,你的工作就是挑出行为最正确的那个。就让编译器厂商为“尽可能降低成本”鞠躬尽瘁吧。
请记住:
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。