从工厂方法和静态变量分配中了解返回值优化 (Visual Studio)

Posted

技术标签:

【中文标题】从工厂方法和静态变量分配中了解返回值优化 (Visual Studio)【英文标题】:Understanding return value optimization from a factory method and static variable assignment (Visual Studio) 【发布时间】:2016-04-23 13:10:45 【问题描述】:

在一个库中,我有一个 A 类,它创建并填充了 C 类的工厂方法。然后将返回的对象分配给 B 类中的静态成员 var。我的问题是返回值优化似乎没有至少在 Visual Studio 中,它在 XCode 中按预期工作(使用 clang)。发生的情况是,在使用定义的移动赋值运算符之前, 调用了类 A 的 d-tor。在 d-tor 中,我释放了 A 中的一些内存,因此之后数据不再有效。

class A 
public:
  A();
  A(const A &other);
  A(const A &&other);

  A& operator = (const A &other);
  A& operator = (const A &&other);
;

class B 
private:
  static A _a;
  struct Initializer 
    Initializer();
  ;
  static Initializer _init;
;

B::Initializer::Initializer() 
  C c;
  _a = c.factoryMethod();


B::Initializer B::_init;

工厂方法很简单:

A C::factoryMethod() 
  A a;
  ....
  return a;

return a 上,调用 A 的复制构造函数,然后调用 d-tor。之后触发移动运算符(将结果分配给 B 中的 _a)。

这与 RVO 应该做的完全相反。永远不应该有副本(因此是破坏,除非 B 被删除)。那么,为什么没有按预期工作?

更新

经过反复试验,我找到了一个适用于 Visual Studio 和 XCode 的解决方案。但是,由于这个问题是关于理解它应该如何工作(以及为什么它在我的情况下不起作用),我仍然对一个好的答案感兴趣。我为解决我的问题所做的是完全删除了复制构造函数,因此它实际上从未被调用过。

两个编译器的行为仍然不同。 VS 使用移动构造函数,而 XCode 没有,但无论如何都需要定义它。另一个奇怪的效果是我现在在 VS 中有 2 次移动(移动构造函数,然后当我分配给静态成员 var 时进行移动分配)。在 XCode 中只有移动赋值。

【问题讨论】:

首先,A(const &&other); 语法无效,不应编译。其次,移动构造函数通常采用非常量右值引用。您的意思可能是 A(A&& other); 与赋值运算符类似。 似乎是work for me。我没有看到问题。 是的,语法错误(我更新了它)。但是对于您的实时示例:正如我所写的,这似乎取决于编译器。是的,它在 XCode (clang) 中按预期工作,它是 Visual Studio 的问题。 return a 在复制构造函数中结束,而不是在移动构造函数中,然后是从同一位置调用 d-tor,然后是移动赋值,此时将返回值分配给静态 var。这很疯狂。顺便提一句。 const 与否在这里没有区别。 好的,确认,这个小例子有效,即使在 VS 中也是如此。我什至可以用我的真实班级替换 A 级,它仍然有效。似乎包含工厂方法的类在这里发挥了作用。当我将小示例更改为原始工厂方法时,事情又开始出错了。 好吧,如果您需要进一步的帮助,您必须制作一个 MCVE,以实际展示您认为仍然存在的任何问题。 【参考方案1】:
static A _a;

这是一个静态成员变量。因此,它将在 main 之前初始化。它已经拥有一个对象。因此,_a = c.factoryMethod(); 不能省略副本。

复制省略只能在您初始化变量时发生。它已经被初始化了,所以它必须被复制到已经初始化的变量中。

当然,此副本应通过调用 operator= 进行。

不,您的_init 对象也是静态的这一事实无济于事。 C++ 定义了 C++ 文件中静态对象的初始化顺序,该顺序是声明顺序。所以B::_a首先被初始化,然后B::_init。所以当B::_init 运行时,B::_a 已经被初始化了。并且颠倒顺序也无济于事,因为您无法分配给尚未开始其生命周期的对象。

如果你真的想用elision做复杂的初始化,那么你需要正确地做:当你在.cpp文件中声明你的静态成员变量时:

A B::_a = []() 
  C c;
  return c.factoryMethod();
()

您必须记住的另一件事是省略是可选的。编译器没有必须这样做,也没有办法强制编译器这样做。

特别是对于命名变量。甚至 C++17 在某些情况下强制执行省略的提议也只对未命名的返回值强制执行。使用提议的 C++17 功能,并使用 lambda 进行适当的初始化,那么您可以保证在最坏的情况下获得一次移动构造函数调用。

【讨论】:

我真的以为就是这样,但不幸的是,这也不是解决方案。首先,来自 Igor 的小例子有效。其次,它在铿锵声中工作。两者都反驳了您的说法,即复制省略只能在初始化时发生。第三,即使使用 lambda,在我的工厂方法中调用 return a 时仍会调用复制构造函数。 @MikeLischke:查看答案的补充内容。 可选的 RVO 确实是个问题,如果你依赖它的话。我在我的 A 类中有许多类实例要删除,这显然会使副本无效(如果调用了副本 c-tor 或分配)。移动构造函数中的 const 不会影响对它的搜索,无论是在 VS 还是在 XCode 中。但是它有一个重要的副作用:移动成员失败,因此您遇到与复制构造函数相同的情况。所以确实,请不要 const :-) 顺便说一句:我喜欢你的 lambda init。比静态结构 init 好得多。

以上是关于从工厂方法和静态变量分配中了解返回值优化 (Visual Studio)的主要内容,如果未能解决你的问题,请参考以下文章

代码优化考虑使用静态工厂方法取代构造器

类加载的过程

工厂方法

字符串优化

lambda 如何在 MSVC2017 15.9.3 中使用 /std:c++17 中的静态局部错误返回值?

Java中成员变量分配在哪个空间?