4

我最近在我的方程求解器中偶然发现了一些奇怪的行为,这让我问自己是否真的理解移动语义和 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;
}

这些是我在运行此代码之前的期望:

  1. Example方法实例化一个本地Foo对象,从而调用默认的ctor
  2. Example按值返回本地Foo对象。但是,由于RVO,我希望此副本会被省略。
  3. 随后对复制 ctor的调用也可能会得到优化。相反a,可能会给出内部临时对象的地址Example
  4. 为了计算表达式a + aoperator+在左侧操作数上调用该方法。
  5. 右侧操作数按值传递,因此可能必须制作本地副本。
  6. 在方法内部,operator+=在该副本上调用并*this通过引用传递。
  7. 现在operator+=再次返回对同一个本地副本的引用,跳回到调用operator+方法的返回语句。
  8. 被引用的对象最终是按值返回的。在这里,我预计另一个副本省略,因为本地副本的值b现在只需要保留(就像之前在步骤 2 和 3 中发生的那样)。
  9. 两个对象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

显然,编译器现在将其识别rhsxvalue(如果我明确地编写它也会这样做)并改为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

据我所知,编译器必须去

  1. RVO(如果可能),或
  2. 移动建筑(如果可能),或
  3. 复制构造(否则),

以该顺序。所以我的问题是:有人可以向我解释一下,步骤 8 中到底发生了什么以及为什么上述优先规则不适用(或者如果是,我在这里看不到的是什么)?很抱歉这个冗长的例子,并在此先感谢。

我目前正在使用 gcc mingw-w64 x86-64 v.4.9.2-std=c++11并关闭优化。

ps - 请抵制就如何编写正确的 OO 代码并确保封装提供建议的冲动;-)

4

2 回答 2

3

按值参数不受 NRVO 约束(为什么按值参数从 NRVO 中排除?)所以它们被移动(值参数在按值返回时是否隐式移动?

一个相当简单的解决方案是通过 const 引用获取两个参数并在函数体中复制:

Foo operator+(Foo const& rhs) const
{
    cout << "Summing Foo objects" << endl;
    Foo res{*this};
    res += rhs;
    return res;
}
于 2015-03-19T18:27:57.750 回答
2

If you want to get rid of temporaries, I suggest you use the following implementation:

Foo operator+(const Foo& rhs) const
{
    cout << "Summing Foo objects" << endl;
    Foo result(rhs);
    result += *this;
    return result;
}

which allows the NRVO to be applied. Your second version might be optimized away by a "Sufficiently Smart Compiler", but mine works today on most compilers. It's not really an issue with the standard, but with the quality of implementation of compilers.

You could also check out libraries like Boost.Operators or df.operators which will implement most of the boiler-plate code for you.

于 2015-03-19T18:16:00.090 回答