我最近在我的方程求解器中偶然发现了一些奇怪的行为,这让我问自己是否真的理解移动语义和RVO是如何协同工作的。
此论坛上有很多related questions,我也在此阅读many general explanations。但我的问题似乎非常具体,所以我希望有人会帮助我。
所涉及的结构完全有点复杂,但它至少打破了这个:
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+
方法的return语句。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
并关闭优化。
P.S。 - 请拒绝告诉我如何编写适当的OO代码并确保封装的冲动; - )
答案 0 :(得分:3)
按值参数不受NRVO(Why are by-value parameters excluded from NRVO?)限制,因此会移动它们(Are value parameters implicitly moved when returned by value?)
一个相当简单的解决方案是通过const引用获取两个参数并在函数体内复制:
Foo operator+(Foo const& rhs) const
{
cout << "Summing Foo objects" << endl;
Foo res{*this};
res += rhs;
return res;
}
答案 1 :(得分:2)
如果你想摆脱临时工,我建议你使用以下实现:
Foo operator+(const Foo& rhs) const
{
cout << "Summing Foo objects" << endl;
Foo result(rhs);
result += *this;
return result;
}
允许应用NRVO。您的第二个版本可能会被一个“足够智能编译器”#34;进行优化,但是今天我的大多数编译器都可以使用它。它并不是标准的问题,而是编译器的实施质量。
您还可以查看Boost.Operators或df.operators等库,这些库将为您实现大部分样板代码。