Google C++每周贴士 #11: 返回策略

(原文链接:https://abseil.io/tips/11 译者:clangpp@gmail.com)

每周贴士 #11: 返回策略

Frodo: There’ll be none left for the return journey. Sam: I don’t think there will be a return journey, Mr. Frodo. – The Lord of the Rings: The Return of the King (novel by J.R.R. Tolkien, screenplay by Fran Walsh, Philippa Boyens, & Peter Jackson)
(译者注:文学水平太洼,起兴这段保留原文)

注:这条贴士——虽然仍然有效——但写它的时候还没有C++11的移动语义。读这条贴士时请同时参阅TotW #77

很多老的C++代码库使用了"害怕复制对象"的范式。幸运的是,多亏了“返回值优化”(RVO),我们可以“复制”但不真的复制。

RVO特性存在已久,几乎所有的C++编译器都实现了它。考虑如下的C++98代码,有一个复制构造函数和一个赋值运算符。这些函数代价都很大,开发者让它们在每次被调用时都打印一条消息:

class SomeBigObject {
 public:
  SomeBigObject() { ... }
  SomeBigObject(const SomeBigObject& s) {
    printf("死贵的复制…\n",);}
  SomeBigObject& operator=(const SomeBigObject& s) {
    printf("死贵的赋值…\n",);return *this;
  }
  ~SomeBigObject() { ... }};

(注:此处故意避免讨论移动操作。参阅TotW #77获取更多信息。)

你会被长成下面这样的工厂方法吓一跳吗?

static SomeBigObject SomeBigObjectFactory(...) {
  SomeBigObject local;
  ...
  return local;
}

看着挺低效地,是不是?如果我们跑下面这段代码会发生什么?

SomeBigObject obj = SomeBigObject::SomeBigObjectFactory(...);

简单的答案:你也许会期望至少有两个对象被构造:一个是函数返回的对象,另一个是函数调用处的(接收函数返回值的,译者注)对象。两次都是复制,所以程序打印两条“死贵”的消息。实践的答案:没有消息被打印——因为复制构造函数和赋值运算符都没有被调用!

这是怎么回事?很多C++程序员为了写“高效代码”,创造一个对象,然后把对象地址传给函数,函数用指针或引用来操作原始对象。其实,在下面的情形中,编译器能把那些“低效的复制”转译成如前的“高效代码”。

当编译器在函数调用处看到一个变量(来接收函数返回值),在被调用的函数里看到另一个(将要被返回的)变量,那编译器就意识到它不需要两个变量。在实现中,编译器就会把函数调用处(函数外,译者注)的变量的地址传递给被调用的函数(函数里,译者注)。

引用C++98标准原文的说法,“当一个临时对象被复制构造函数复制时…(编译器,译者注)实现被(本标准,译者注)允许将原始对象和副本对象看做指向同一对象的两种方式,因此不需要执行复制操作——即使复制构造函数或析构函数有副作用(例如打印消息,修改全局计数器等,译者注)。对于一个返回值为类的函数,如果返回语句中的表达式是局部对象的名字…(编译器,译者注)实现被(本标准,译者注)允许不必创建临时对象来保存函数返回值…”(C++98标准,第12.8节[class.copy],第15段。C++11标准在第12.8节,第31段有类似的描述,但是更复杂。)

担心“被允许”不是个很强的承诺?幸运的是,所有现代C++编译器默认都会执行RVO,即使是在调试版本,即使是非内联函数。

你怎么保证编译器执行RVO?

被调用的函数应该定义唯一的返回值变量:

SomeBigObject SomeBigObject::SomeBigObjectFactory(...) {
  SomeBigObject local;return local;
}

函数调用处应该将返回值赋值给一个新的变量:

// 不会打印“死贵”操作的消息:
SomeBigObject obj = SomeBigObject::SomeBigObjectFactory(...);

就这些!

如果函数调用处重用已经存在的变量来存储返回值,那么编译器没法执行RVO(虽然这种情况下定义了移动操作的类型会执行移动语义):

// RVO不会发生;打印详细“死贵的赋值...”:
obj = SomeBigObject::SomeBigObjectFactory(s2);

如果被调用的函数使用多于一个变量作为返回值,那么编译器也没法执行RVO:

// RVO不会执行:
static SomeBigObject NonRvoFactory(...) {
  SomeBigObject object1, object2;
  object1.DoSomethingWith(...);
  object2.DoSomethingWith(...);
  if (flag) {
    return object1;
  } else {
    return object2;
  }
}

但如果被调用的函数在多个地方返回同一个变量,编译器会执行RVO:

// RVO会执行:
SomeBigObject local;
if (...) {
  local.DoSomethingWith(...);
  return local;
} else {
  local.DoSomethingWith(...);
  return local;
}

关于RVO,以上大概就是你需要了解的一切。

还有一件事:临时变量

除了命名变量,RVO也对临时变量执行。当被调用的函数返回临时变量时,你仍然可以从RVO中获益:

// RVO会执行:
SomeBigObject SomeBigObject::ReturnsTempFactory(...) {
  return SomeBigObject::SomeBigObjectFactory(...);
}

当函数调用处(以临时变量的形式)直接使用返回值时,你仍然可以从RVO中获益:

// 不会打印“死贵”操作的消息:
EXPECT_EQ(SomeBigObject::SomeBigObjectFactory(...).Name(), s);

最后的注解:如果你的代码需要执行复制,那就执行复制,不论这些复制会不会被优化掉。不要牺牲正确性来换取效率。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值