我最近在我的方程求解器中偶然发现了一些奇怪的行为,这让我问自己是否真的理解移动语义和 RVO 是如何协同工作的。
这个论坛上有很多相关的问题,我也阅读了很多关于这个的一般解释。但我的问题似乎很具体,所以我希望有人能帮助我。
涉及的结构有点复杂,但至少可以分解为:
struct Foo
{
Bar* Elements;
Foo(void) : Elements(nullptr)
{
cout << "Default-constructing Foo object " << this << endl;
}
Foo(Foo const& src) : Elements(nullptr)
{
cout << "Copying Foo object " << &src << " to new object " << this << endl;
if (src.Elements != nullptr)
{
Allocate();
copy (src.Elements, src.Elements + SIZE, Elements);
}
}
Foo(Foo&& src) : Elements(nullptr)
{
cout << "Moving Foo object " << &src << " into " << this << endl;
Swap(src);
}
~Foo(void)
{
cout << "Destructing Foo object " << this << endl;
Deallocate();
}
void Swap(Foo& src)
{
cout << "Swapping Foo objects " << this << " and " << &src << endl;
swap(Elements, src.Elements);
}
void Allocate(void)
{
Elements = new Bar[SIZE]();
}
void Deallocate(void)
{
delete[] Elements;
}
Foo& operator=(Foo rhs)
{
cout << "Assigning another Foo object to " << this << endl;
Swap(rhs);
return *this;
}
Foo& operator+=(Foo const& rhs)
{
cout << "Adding Foo object " << &rhs << " to " << this << endl;
// Somehow adding rhs to *this
cout << "Added Foo object" << endl;
return *this;
}
Foo operator+(Foo rhs) const
{
cout << "Summing Foo objects" << endl;
return rhs += *this;
}
static Foo Example(void)
{
Foo result;
cout << "Creating Foo example object " << &result << endl;
// Somehow creating an 'interesting' example
return result;
}
};
现在让我们考虑以下短程序:
int main()
{
Foo a = Foo::Example();
cout << "Foo object 'a' is stored at " << &a << endl;
Foo b = a + a;
cout << "Foo object 'b' is stored at " << &b << endl;
}
这些是我在运行此代码之前的期望:
- 该
Example
方法实例化一个本地Foo
对象,从而调用默认的ctor。 Example
按值返回本地Foo
对象。但是,由于RVO,我希望此副本会被省略。- 随后对复制 ctor的调用也可能会得到优化。相反
a
,可能会给出内部临时对象的地址Example
。 - 为了计算表达式
a + a
,operator+
在左侧操作数上调用该方法。 - 右侧操作数按值传递,因此可能必须制作本地副本。
- 在方法内部,
operator+=
在该副本上调用并*this
通过引用传递。 - 现在
operator+=
再次返回对同一个本地副本的引用,跳回到调用operator+
方法的返回语句。 - 被引用的对象最终是按值返回的。在这里,我预计另一个副本省略,因为本地副本的值
b
现在只需要保留(就像之前在步骤 2 和 3 中发生的那样)。 - 两个对象
a
最终b
都将超出范围,因此调用它们的析构函数。
令人惊讶的观察结果(至少对我而言)是,在第 8 步中,深层副本没有被优化(无论使用什么编译器选项)。相反,输出如下所示:
01 Default-constructing Foo object 0x23fe20
02 Creating Foo example object 0x23fe20
03 Foo object 'a' is stored at 0x23fe20
04 Copying Foo object 0x23fe20 to new object 0x23fe40
05 Summing Foo objects
06 Adding Foo object 0x23fe20 to 0x23fe40
07 Added Foo object
08 Copying Foo object 0x23fe40 to new object 0x23fe30
09 Destructing Foo object 0x23fe40
10 Foo object 'b' is stored at 0x23fe30
11 Destructing Foo object 0x23fe30
12 Destructing Foo object 0x23fe20
在我看来,以下小的变化operator+
根本没有任何区别:
Foo operator+(Foo rhs) const
{
cout << "Summing Foo objects" << endl;
rhs += *this;
return rhs;
}
然而,这一次的结果完全不同:
01 Default-constructing Foo object 0x23fe20
02 Creating Foo example object 0x23fe20
03 Foo object 'a' is stored at 0x23fe20
04 Copying Foo object 0x23fe20 to new object 0x23fe40
05 Summing Foo objects
06 Adding Foo object 0x23fe20 to 0x23fe40
07 Added Foo object
08 Moving Foo object 0x23fe40 into 0x23fe30
09 Swapping Foo objects 0x23fe30 and 0x23fe40
10 Destructing Foo object 0x23fe40
11 Foo object 'b' is stored at 0x23fe30
12 Destructing Foo object 0x23fe30
13 Destructing Foo object 0x23fe20
显然,编译器现在将其识别rhs
为xvalue(如果我明确地编写它也会这样做)并改为return move(rhs += *this);
调用move ctor 。
此外,使用该-fno-elide-constructors
选项,您将始终获得以下信息:
01 Default-constructing Foo object 0x23fd30
02 Creating Foo example object 0x23fd30
03 Moving Foo object 0x23fd30 into 0x23fe40
04 Swapping Foo objects 0x23fe40 and 0x23fd30
05 Destructing Foo object 0x23fd30
06 Moving Foo object 0x23fe40 into 0x23fe10
07 Swapping Foo objects 0x23fe10 and 0x23fe40
08 Destructing Foo object 0x23fe40
09 Foo object 'a' is stored at 0x23fe10
10 Copying Foo object 0x23fe10 to new object 0x23fe30
11 Summing Foo objects
12 Adding Foo object 0x23fe10 to 0x23fe30
13 Added Foo object
14 Moving Foo object 0x23fe30 into 0x23fe40
15 Swapping Foo objects 0x23fe40 and 0x23fe30
16 Moving Foo object 0x23fe40 into 0x23fe20
17 Swapping Foo objects 0x23fe20 and 0x23fe40
18 Destructing Foo object 0x23fe40
19 Destructing Foo object 0x23fe30
20 Foo object 'b' is stored at 0x23fe20
21 Destructing Foo object 0x23fe20
22 Destructing Foo object 0x23fe10
据我所知,编译器必须去
- RVO(如果可能),或
- 移动建筑(如果可能),或
- 复制构造(否则),
以该顺序。所以我的问题是:有人可以向我解释一下,步骤 8 中到底发生了什么以及为什么上述优先规则不适用(或者如果是,我在这里看不到的是什么)?很抱歉这个冗长的例子,并在此先感谢。
我目前正在使用 gcc mingw-w64 x86-64 v.4.9.2-std=c++11
并关闭优化。
ps - 请抵制就如何编写正确的 OO 代码并确保封装提供建议的冲动;-)