【问题标题】:Does return value optimization work, when assigning to a different type?当分配给不同的类型时,返回值优化是否有效?
【发布时间】:2011-04-24 14:48:25
【问题描述】:

考虑以下两个类:

class Base  
{  
  Base(const Base& other) {...} // relatively expensive operations here...
  Base(int i)             {...} // ...here,
  virtual ~Base()         {...} // ...and here
  ...
};

class Derived : public Base
{
  ...
  Derived(const Base& other)   :Base(other) {...} // some typechecking in here 
  virtual ~Derived() {}
  ...
};

这意味着 Base 可以通过 Derived 的第二个构造函数“向上转换”。 现在考虑以下代码:

Base getBase()  
{
   int id = ...
   return Base(id);
}
...
int main()
{
   Base    b = getBase();   // CASE 1
   Derived d1(b);           // "upcast"

   Derived d2 = getBase();  // CASE 2
   ...
}

我正在使用开启优化的 VS2008 (/Ox /Ob2 /Oi /Ot)。我检查了控制台输出上对构造函数和析构函数的调用:

案例 1 中,返回值优化有效。有两个调用:

  1. 基数(整数)
  2. ~Base()

然而,当 main 中需要 Derived 对象时,这里没有什么可取胜的。 “向上转型”需要另一个构造函数/析构函数对。

案例 2 中,返回值优化不起作用。这里创建和销毁了两个对象:

  1. Base(int) //创建临时的
  2. ~Base() //临时销毁
  3. Base(const Base&) //通过 Derived(const Base&)
  4. ~Base() //通过~Derived()

现在在我看来,我有三个相互矛盾的要求:

  1. 我想避免创建临时对象的开销(因为对象创建和销毁在类 Base 中相当昂贵)
  2. main 中,我需要使用 Derived-object 而不是 Base-object。

显然,这里没有免费的午餐。但我可能错过了一些东西。所以我的问题是:有没有办法结合这些要求?或者有没有人有过类似的经历?

旁注:我知道这样一个事实,即“upcast” Derived(const Base& other) 在运行时可能会失败(这已被处理)。由于代码在语法级别上没问题,我猜这不是编译器避免 RVO 的原因。

【问题讨论】:

  • RVO是指在创建返回值时,通过函数内部的return语句消除“需要”的副本。如果将返回值用作对象的初始化器,则可以将返回值复制到声明的对象中,或者可以消除该副本并直接使用函数的返回值初始化对象。第二个副本不是返回值优化,我认为您指的是这个副本。
  • 所以在情况 1 中没有调用 Base 的复制 ctor?我不认为这是合法的——bd1 是独立的对象,d1 的基类子对象需要从b 构造。复制构造函数省略只允许省略临时变量,而不是变量。 [编辑:哦,等等,你的意思是有一个但你没有列出它? “需要另一个 ctor/dtor 对”。我会说 RVO 正在工作,但正如你所见,它并没有达到你想要的效果。]

标签: c++ return-value-optimization


【解决方案1】:

这很糟糕。

Derived(const Base& other)   :Base(other) {...}

other 的静态类型可以属于派生类型。在这种情况下,它会被切片。在那个基类之上无论如何都会被复制。

RVO 是关于绕过复制构造函数并就地初始化返回的对象。如果您需要派生类型的对象,则必须先构造它。 RVO 无法为您构建它。

您可能需要考虑不同的方法,而不是 Derived(const Base& other)。这个怎么样:

class Base  
{
  ...
  // extract expensive parts of another instance
  virtual void initialise(Base& b);
  ...
};

class Derived : public Base
{
  ...
  Derived(); // cheap constructor
  void initialise(Base& b) { /* implementation goes here */  }
  ...
};

initialise(Base& b) 方法将从参数中提取昂贵的部分。它可能具有破坏性。 Base 将提供公共(或可能受保护的)接口来进行实际提取。

【讨论】:

    【解决方案2】:

    Derived添加一个构造函数怎么样?

    Derived(Base (*f)(void)) : Base(f()) { ... }
    

    那么Derived d3 = getBase; 可能会为您提供所需的优化。可能不是很实用,因为我必须在Derived 中指定f 的空参数列表,这是非常有限的。但将其设为模板构造函数,您就可以使用用户编写的函子、boost:bind 的结果或可用的 C++0x lambda。

    如果做不到这一点,请将getBaseid = ... 部分提取到函数getId 中,并为Derived 提供一个采用int 的构造函数,该构造函数将id 传递给它的Base 子对象。毫无疑问,真正的代码比这更复杂,这可能会导致大量关于这个地方的参数。也许是一个轻量级的 BaseParameters 类,你可以用它来代替 Base 直到你真正需要完成缓慢的工作,然后将 that 转换为 BaseDerived 或另一个相关的类。

    【讨论】:

    • 听起来很有趣。我要试试这个看看效果。然而,这会在“派生”类和函数“getBase()”(实际上代表许多类似的函数)之间引入耦合,我想避免这种耦合。
    • @osterhase:取决于你是怎么做的。如果getBase 的所有参数都被bind 或lambda 捕获,那么Derived 仍然只通过接口耦合到Base,而不是getBase。任何返回 Base 的无参数函数都可以,这足以将 Derived 与调用者选择传递的函数及其工作方式的细节隔离开来。这是一个依赖注入,恰好只在初始化列表中使用一次。
    • 但是由于它被认为是一种希望获得优化的方式,而不是作为所需界面的必要部分,显然它在某些方面可能会令人不快......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-10-26
    • 2016-06-09
    • 2011-12-22
    • 1970-01-01
    • 2013-12-16
    • 1970-01-01
    • 2022-06-25
    相关资源
    最近更新 更多