学完C/C++编程:宁以pass-by-reference-to-const替换pass-by-value之后,你可能会想用pass-by-reference替换所有的pass-by-value,但这个过程中,你一定会犯下一个致命错误:开始传递一些reference指向并不存在的对象。
看个例子:
class Rational{ // 有理数类
public:
Rational(int numrater = 0, int denominator = 1);
private:
int n, d; // 分子和分母
friedn:
const Rational operator* (const Rational& lhs, const Rational& rhs); // 一定要是const
}
这个版本的operator*是以pass-by-value的方式返回计算结果(一个对象)。那么该对象的构造和析构成本是什么,需要付出什么代价?为了避免这种代价,你可能会想要返回一个指针或者一个引用。
-
可以确定的是,如果pass-by-reference,就不需要付出代价。但是注意,所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,你都应该立即问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。以上面operator*为例,如果它返回一个reference,后者一定指向某个既有Rational对象,内含两个Rational对象的乘积。
-
我们当然不可能期望这样一个(内含乘积的)Rational对象在调用operator*之前就存在。也就是说,如果你有:
Rational a(1, 2);
Rational b(3, 5);
Rational c = a * b; // c= 3/ 10
- 期望"原本就存在一个其值为3/10的Rational 对象"不合理。如果operator*要返回一个引用指向如此数值,它必须自己创建那个Rational 对象。
错误写法
函数创建新对象的途径有:在stack空间或者heap空间创建。如果定义一个local变量,就是在stack空间创建对象。也就是说:
const Rational operator* (const Rational& lhs, const Rational& rhs){
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 警告,糟糕的代码
return result;
}
这个函数调用了构造函数,更严重的是,返回一个reference指向local对象(result),但是local对象在推出前就被销毁了。任何调用者只要使用这个返回值,就会"无定义行为"。实际上,任何函数如果返回一个reference/pointer指向一个local对象,就会"无定义行为"
于是,让我们在heap内构造一个对象,并返回reference指向它:
const Rational operator* (const Rational& lhs, const Rational& rhs){
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 警告,更糟的写法
return result;
}
这个函数还是付出了调用一个构造函数的代价(因为分配所得的内存将以一个适当的构造函数完成初始化操作)。更严重的问题是,谁来delete这个new出来的对象
Retion w, x, y, z;
w = x * y * z;
上面,同一个语句内调用了两次operator*
,因而两次使用new,也就需要两次delete,但是没有合理的方法取得operator*返回的引用背后的那个指针,这绝对导致资源泄漏。
上面不管是on-the-stack或者on-the-heap做法,都因为operator*
返回的结果而受到惩罚,也许你会想”让operator*返回的引用指向一个被定义于函数内部的static Rational“就不用析构了:
const Rational operator* (const Rational& lhs, const Rational& rhs){
static Rational result; // 更糟的代码
// ...
return result;
}
先不谈多线程安全性,就看下面的代码:
Rational a, b, c,d;
if((a * b) == (c * b)){ //
}
上面==结果当永远是true,而不管a,b,c,d是什么。
正确实践
从上面所有,就可以看出,想要比如operator*这样的函数返回reference,只是浪费时间。一个必须返回新对象的函数的正确写法是:就让那个函数返回一个新对象
const Rational operator* (const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
仔细观察被返回的表达式,它看上去好像正在调用Rational的构造函数,实际上确是这样。你通过这个表达式建立一个临时的Rational对象,
Rational(lhs.n * rhs.n, lhs.d * rhs.d);
并且这是一个临时对象,函数把它拷贝给函数的返回值。
返回constructor argument而不出现局部对象,这种方法还会给你带来很多开销,因为你仍然必须在函数内临时对象/函数返回对象的构造和析构而付出代价。C++规则允许编译器优化不出现的临时对象(temporary objects out of existence)。因此如果你在如下的环境里调用operator*:
Rational a = 10;
Rational b(1, 2);
Rational c = a * b;
编译器就会被允许消除在operator*
内的临时变量和operator*
返回的临时变量。它们能在为目标c分配的内存里构造return表达式定义的对象。如果你的编译器这样去做,调用operator*
的临时对象的开销就是零:没有建立临时对象。你的代价就是调用一个构造函数――建立c时调用的构造函数。而且你不能比这做得更好了,因为c是命名对象,命名对象不能被消除。不过你还可以通过把函数声明为inline来消除operator*
的调用开销
inline const Rational operator* (const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
这种特殊的优化――通过使用函数的return 位置(或者在函数被调用位置用一个对象来替代)来消除局部临时对象――是众所周知的和被普遍实现的。它甚至还有一个名字:返回值优化(return value optimization)(在《深度探索C++对象模型》中有更多更详细的讲述,它叫之为named return value optimization)。
但注意,这种优化对普通的赋值运算无效,编译器不能够用拷贝构造函数取代赋值运算动作,最终结论是:在确保语意正确的前题下没有更好的优化可能了)。实际上这种优化有自己的名字本身就可以解释为什么它被广泛地使用。
总结
- 绝不要在pointer或者reference指向一个local stack对象,或者返回reference指向一个heap-allocated对象,或者返回pointer/reference指向一个local static对象(有可能同时需要这样的对象)
- C/C++编程:确定对象在使用前就已经先被初始化了 提供了”怎么在单线程环境中合理返回reference指向一个local static对象“的设计实例