本周小贴士#11:返回策略

作为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);

结束语:如果您的代码需要制作副本,那么无论是否可以优化副本,都请制作副本。 不要为了效率而牺牲正确性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值