本周小贴士#166:当复制不是复制

作为TotW#166最初发表于2020年4月6日

由Richard Smith创作

"实体不应多于所需。”——威廉.奥卡姆

“如果你不知道要去哪里,你可能走错路了。”——特里.普拉切特

概述

从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)使用评估初始化程序时找到的指令直接初始化。如果名称是引用,则将创建一个临时对象来保存值。

因此,对象直接在正确的位置构造,而不是在其他地方构造然后复制。这种行为有时被称为“保证复制省略”,但这是不准确的:一开始根本没有复制。

您需要知道的是:只有在对象首次被赋予名称后才进行复制。返回值没有额外的成本。

(即使在被赋予名称后,局部变量在从函数返回时可能仍不会复制,这是由于命名返回值优化。有关详细信息,请参见提示11。)

细节探究:无名对象何时被复制

有两种情况下,使用没有名称的对象会导致复制:

  • 构造基类:在构造函数的基类初始化列表中,即使从基类类型的未命名表达式构造,也将进行复制。这是因为作为基类时,类可能具有不同的布局和表示(由于虚基类和vpointer值),因此直接初始化基类可能不会得到正确的表示。
class DerivedThing : public BigExpensiveThing {
 public:
  DerivedThing() : BigExpensiveThing(MakeAThing()) {}  // might copy data_
};
  • 传递或返回小的微不足道的对象:当传递到函数中或从函数返回一个足够小而又是平凡可复制的对象时,它可能会被传递到寄存器中,因此在传递前后可能会有不同的地址。
struct Strange {
  int n;
  int *p = &n;
};
void f(Strange s) {
  CHECK(s.p == &s.n);  // might fail
}
void g() { f(Strange{0}); }

细节探究:值类别

C++中有两种表达式:

  • 产生值的表达式,比如1或MakeAThing()——你可能认为这些表达式具有非引用类型。
  • 产生某个现有对象位置的表达式,比如s或thing.data_[5]——你可能认为这些表达式具有引用类型。
    这个区分称为“值类别”;前者是prvalue,后者是glvalue。当我们上面谈到没有名称的对象时,我们真正指的是prvalue表达式。

所有的prvalue表达式都在某个上下文中进行求值,该上下文决定了它们放置其值的位置,并且prvalue表达式的执行使用它的值初始化该位置。

例如,在

BigExpensiveThing thing = MakeAThing();

中,prvalue表达式MakeAThing()作为thing变量的初始化器进行求值,因此MakeAThing()将直接初始化thing。构造函数将一个指向thing的指针传递给MakeAThing(),并且MakeAThing()中的return语句初始化指针所指向的任何内容。同样,在

return BigExpensiveThing();

中,编译器有一个要初始化的对象指针,并通过调用BigExpensiveThing构造函数直接初始化该对象。

你可能感兴趣的:(C++,Tips,of,the,Week,c++,开发语言)