为什么既不移动语义也不会按预期工作?

我最近在我的方程求解器中偶然发现了一些奇怪的行为,这让我问自己是否真的理解了移动语义和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中所发生的那样)。
  • 对象ab最终都会超出范围,因此调用它们的析构函数。
  • 令人惊叹的观察(至少对我而言)是,在步骤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代码并确保封装;-)


    按值参数不受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