作为totw/11最初发表于2012年8月16日
作者Paul S. R. Chisholm (p.s.r.chisholm@google.com)
弗罗多:回程将一无所有。萨姆:我不认为将有回程,弗罗多先生。——指环王:国王归来(J.R.R. Tolkien的小说,Fran Walsh,Philippa Boyens和Peter Jackson的剧本)
注意:尽量本技巧依然相关,但在C++11引入移动语义之前。请在阅读此技巧时,同时记住来自TotW#77的建议。
许多较旧的的C++代码库表现出有些担心拷贝对象的模式。幸运的是,由于有了“返回值优化”(RVO)之类的东西,我们可以“拷贝”而不用拷贝。
几乎所有的C++编译器都有一个持续的特性名为RVO。考虑到下面的C++98代码,它具有一个拷贝构造函数和一个赋值操作运算符。这些函数的开销很大,开发人员每次使用时都会让它们打印一条消息。
class SomeBigObject {
public:
SomeBigObject() { ... }
SomeBigObject(const SomeBigObject& s) {
printf("Expensive copy …\n", …);
…
}
SomeBigObject& operator=(const SomeBigObject& s) {
printf("Expensive assignment …\n", …);
…
return *this;
}
~SomeBigObject() { ... }
…
};
(请注意,我们刻意避免在此讨论移动操作。请参见TotW#77。)
如果此类有一个如下的工厂方法,你会畏惧吗?
static SomeBigObject SomeBigObjectFactory(...) {
SomeBigObject local;
...
return local;
}
看起来是低效的,不是吗?如果我们运行下面的代码,会发生什么呢?
SomeBigObject obj = SomeBigObject::SomeBigObjectFactory(...);
简单的答案:你可能期望至少创建两个对象:一个对象来自被调用的函数,一个对象在调用函数中。它们都是副本,因此程序会打印两条有关开销大的消息。实际的答案:没有消息打印,因为拷贝构造函数和赋值运算符都没有被调用。
那是如何发生的呢?许多C++程序员编写高效的代码来创建一个对象并将对象的地址传递给函数,该函数使用指针或引用来操作原始的对象。好吧,在下面描述的情况下,编程器能够将这种“低效的拷贝”转变为“高效的代码”!
当编译器在调用函数中看到一个变量(从返回值来构造),在被调用函数中看到一个变量(将返回)时,它意识它不需要两个变量。在底下,编译器传递调用函数变量的地址给被调用函数。
引用C++98标准,“任何时候使用拷贝构造函数复制一个临时对象…允许实现将原始对象和副本视为引用相同对象的两种不同方式,而根本不是执行复制,即便类的拷贝构造函数和析构函数有副作用。对于一个返回类型为类的函数,如返回语句中的表达式是本地对象的名称…则允许省略创建临时对象用来保存函数的返回值…”(第12.8节[class.copy],c++98标准中第15段)。C++11标准在第12.8节第32段有类似的语言,但是它更复杂。)
担心“允许”不是一个非常强的承诺吗?幸运的是,所有的现代C++编译器都是默认执行RVO的,即便在调试构建中,甚至于非内联函数也是如此。
你如何确保编译器执行了RVO?
被调用函数应该定义一个简单的变量作为返回值:
SomeBigObject SomeBigObject::SomeBigObjectFactory(...) {
SomeBigObject local;
…
return local;
}
调用函数应该将返回值赋值给一个新的变量:
// 没有关于expensive操作的信息:
SomeBigObject obj = SomeBigObject::SomeBigObjectFactory(...);
就这些!
如果调用函数复用一个存在的变量来存储返回值(尽管在这种情况下移动语义将应用可移动类型),那么编译不执行RVO。
// RVO不会发生在这里,打印"Expensive assignment ...":
obj = SomeBigObject::SomeBigObjectFactory(s2);
如果被调用函数使用多于1个变量作为返回值,那么编译器也不执行RVO。
// RVO不会发生在这里:
static SomeBigObject NonRvoFactory(...) {
SomeBigObject object1, object2;
object1.DoSomethingWith(...);
object2.DoSomethingWith(...);
if (flag) {
return object1;
} else {
return object2;
}
}
但是,如果被调用函数使用一个变量在多处返回,那么它同样是正确的。
// RVO将发生在这里:
SomeBigObject local;
if (...) {
local.DoSomethingWith(...);
return local;
} else {
local.DoSomethingWith(...);
return local;
}
这可能是你需要知道的关于RVO的全部。
还有一件事:临时变量
RVO对临时变量起效,不仅仅是具名变量。当一个被调用的函数返回一个临时对象时,你也能从RVO中受益。
// RVO在此生效:
SomeBigObject SomeBigObject::ReturnsTempFactory(...) {
return SomeBigObject::SomeBigObjectFactory(...);
}
当一个调用函数立即使用返回值(它存储在临时对象中),你也能从RVO中受益。
// 没有关于expensive操作的消息:
EXPECT_EQ(SomeBigObject::SomeBigObjectFactory(...).Name(), s);
结束语:如果您的代码需要制作副本,那么无论是否可以优化副本,都请制作副本。 不要为了效率而牺牲正确性。