为什么既不移动语义也不会按预期工作?
我最近在我的方程求解器中偶然发现了一些奇怪的行为,这让我问自己是否真的理解了移动语义和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的原因,我预计这个副本将被取消。 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
从我相信,编译器必须去
以该顺序。 所以我的问题是:有人可以向我解释第8步中究竟发生了什么,以及为什么上述优先规则不适用(或者如果是这样,我在这里没有看到什么)? 对不起,详细的例子,并提前感谢。
我目前使用gcc mingw-w64 x86-64 v.4.9.2和-std=c++11
并关闭优化。
PS - 请耐心劝告我如何编写适当的OO代码并确保封装;-)
按值参数不受NRVO(为什么按值参数从NRVO中排除?),因此它们被移动(值返回值时是否隐式移动了值参数)?
一个相当简单的解决方案是通过const引用并在函数体内复制两个参数:
Foo operator+(Foo const& rhs) const
{
cout << "Summing Foo objects" << endl;
Foo res{*this};
res += rhs;
return res;
}
如果你想摆脱临时,我建议你使用下面的实现:
Foo operator+(const Foo& rhs) const
{
cout << "Summing Foo objects" << endl;
Foo result(rhs);
result += *this;
return result;
}
这允许NRVO被应用。 你的第二个版本可能会被一个“足够聪明的编译器”优化掉,但是我的编译器现在可以在大多数编译器上运行。 这不是标准问题,而是编译器实现的质量。
你也可以看看像Boost.Operators或df.operators这样的库,它们将为你实现大部分的锅炉代码。
链接地址: http://www.djcxy.com/p/86817.html上一篇: Why do neither move semantics nor RVO work as expected?
下一篇: subscripted value is neither array nor pointer nor vector with argv