为什么既没有移动语义也没有按预期运行RVO?

时间:2015-03-19 17:52:10

标签: c++ c++11 gcc move-semantics rvo

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

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

  1. Example方法实例化本地Foo对象,导致默认ctor 被调用。
  2. Example按值返回本地Foo对象。但是,由于 RVO ,我希望此副本可以省略。
  3. 随后对 copy ctor 的调用也可能会被优化。相反,a可能会被赋予Example内的临时对象的地址。
  4. 为了评估表达式a + a,在左侧操作数上调用operator+方法。
  5. 右侧操作数按值传递,因此可能需要制作本地副本。
  6. 在方法内部,operator+=在该副本上调用*this通过引用传递。
  7. 现在operator+=再次返回对同一本地副本的引用,跳回到调用operator+方法的return语句。
  8. 最终按值返回引用的对象。在这里,我预计会有另一个副本省略,因为本地副本的价值现在只需要由b保留(正如之前在步骤2和3中所发生的那样)。
  9. 对象ab最终都会超出范围,从而调用它们的析构函数。
  10. 围绕观察(至少对我而言)是,在步骤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
    

    据我所知,编译器必须选择

    1. RVO (如果可能)或
    2. 移动施工(如果可能)或
    3. 复制构造(否则),
    4. 按此顺序。所以我的问题是:有人可以向我解释一下,在第8步中真正发生了什么以及为什么上述优先规则不适用(或者如果是这样,我在这里看不到的是什么)?对于详细的例子感到抱歉,并提前感谢。

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

      P.S。 - 请拒绝告诉我如何编写适当的OO代码并确保封装的冲动; - )

2 个答案:

答案 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.Operatorsdf.operators等库,这些库将为您实现大部分样板代码。