Google C++每周贴士 #166: 你不是真正的复制

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

每周贴士 #166: 你不是真正的复制

Entia non sunt multiplicanda praeter necessitatem." (“Entities should not be multiplied without necessity”) – William of Ockham
如无必要,勿增实体——奥卡姆

If you don’t know where you’re going, you’re probably going wrong.” – Terry Pratchett
如果你不知道要去哪儿,那你可能是走错了——特里·普拉切特

概述

从C++17开始,对象会尽可能在“原地”被创建。

class BigExpensiveThing {
 public:
  static BigExpensiveThing Make() {
    // ...
    return BigExpensiveThing();
  }
  // ...
 private:
  BigExpensiveThing();
  std::array<OtherThing, 12345> data_;
};

BigExpensiveThing MakeAThing() {
  return BigExpensiveThing::Make();
}

void UseTheThing() {
  BigExpensiveThing thing = MakeAThing();
  // ...
}

这段代码复制或移动了BigExpensiveThing多少次?

在C++17以前,答案是最多三次:每个return语句一次,还有一次是在初始化thing的时候。这有点道理:每个函数都有可能把BigExpensiveThing放到不同的地方,所以也许需要一次移动,来把值放到最终调用者需要的地方。然而在实践中,对象总是“原地”构造在变量thing里,没有发生任何移动,而且C++语言允许这些移动操作被“省略”,以促成此优化。

在C++17里,这段代码保证零次复制或移动。实际上,就算BigExpensiveThing是不可移动的,上述代码仍然是合法的。BigExpensiveThing::Make中的构造函数调用,直接在UseTheThing里构造本地变量thing

所以发生了什么?

当编译器看到形如BigExpensiveThing()的表达式的时候,它并不会马上创建一个临时对象。取而代之的是,它把这个表达式当做构造某个最终对象的施工图,然后把创建(正式地说,“实现”)临时对象的操作推迟得越长越好。

一般情况下,创建对象会被一直推迟,直到对象被取名。该命名对象(上面例子中的thing)是直接被运行初始化器所得到的施工图所初始化的。如果该名字是一个引用,那么一个临时对象会被实现出来以承载该值。

结果就是,对象在正确的位置被直接构造,而不是在别处构造再复制过来。该行为有时被称作“保证执行的复制省略”,但那是不准确的:从来就没有过复制。

你需要知道的就是:对象在被首次被取名前不会被复制。以值返回没有额外代价。

(就算是被取名以后,函数本地变量被返回时也不会被复制,因为有命名返回值优化。详情请参考Tip 11。)

繁琐细节:匿名对象何时被复制

在两个边缘情况下,使用匿名对象还是会导致复制:

  • 构造基类:在基类构造函数初始化列表中,即使是以基类类型的未命名表达式来构造,还是会发生复制。这是因为类型作为基类时,可能有稍微不同的布局和表现(因为虚基类和虚函数指针值),所以直接初始化基类也许不会得到正确的表现。

    class DerivedThing : public BigExpensiveThing {
    public:
      DerivedThing() : BigExpensiveThing(MakeAThing()) {}  // 可能复制data_
    };
    
  • 传递或返回小而简单的对象:如果一个足够小且可以平凡复制(trivially copyable)的对象,被传递进或返回出函数,那么它可能会被传递进寄存器,因此在传递前后可能会有不同的地址。

    struct Strange {
      int n;
      int *p = &n;
    };
    void f(Strange s) {
      CHECK(s.p == &s.n);  // 可能炸锅
    }
    void g() { f(Strange{0}); }
    

繁琐细节:值类别

C++中有两种风格的表达式:

  • 得到一个值,例如1,或MakeAThing()——你可能会认为拥有非引用类型的表达式。
  • 得到一个指向存在的对象的位置,例如sthing.data_[5]——你可能会认为拥有引用类型的表达式。

这个分割被称为“值类别”;前者是 纯右值(prvalues) 而后者是 泛左值(glvalues)。当我们前面探讨匿名对象的时候,我们实际上指的是纯右值表达式。

所有的纯右值表达式都在决定值存储位置的上下文中被求值,而纯右值表达式的执行过程则被用以在该位置初始化该值。

例如,在

  BigExpensiveThing thing = MakeAThing();

中,纯右值表达式MakeAThing()作为变量thing的初始化器被求值,所以MakeAThing()会直接初始化thing。构造函数把指向thing的指针传递给MakeAThing(),然后MakeAThing()中的return语句初始化该指针指向的玩意儿。相似地,在

  return BigExpensiveThing();

中,编译器有个指针指向待初始化的对象,并且直接调用BigExpensiveThing的构造函数来初始化该对象。

相关阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值