37

据我了解,添加移动语义的目的之一是通过调用特殊构造函数来复制“临时”对象来优化代码。例如,在这个答案中,我们看到它可以用来优化这些string a = x + y东西。因为 x+y 是一个右值表达式,我们可以只复制指向字符串的指针和字符串的大小,而不是深度复制。但正如我们所知,现代编译器支持返回值优化,因此如果不使用移动语义,我们的代码根本不会调用复制构造函数。

为了证明这一点,我编写了以下代码:

#include <iostream>

struct stuff
{
        int x;
        stuff(int x_):x(x_){}
        stuff(const stuff & g):x(g.x)
        {
                std::cout<<"copy"<<std::endl;
        }
};   
stuff operator+(const stuff& lhs,const stuff& rhs)
{
        stuff g(lhs.x+rhs.x);
        return g;
}
int main()
{
        stuff a(5),b(7);
        stuff c = a+b;
}

在 VC++2010 和 g++ 中以优化模式执行它之后,我得到了空输出。

如果没有它,我的代码仍然运行得更快,它是什么样的优化?你能解释一下我理解错了吗?

4

8 回答 8

27

移动语义不应该被认为是一种优化设备,即使它们可以这样使用。

如果您想要对象的副本(函数参数或返回值),那么 RVO 和复制省略将在可能的情况下完成这项工作。移动语义可以提供帮助,但比这更强大。

当您想要做一些不同的事情时,移动语义很方便,无论传递的对象是临时对象(然后绑定到右值引用)还是具有名称的“标准”对象(所谓的const lvalue)。例如,如果您想窃取临时对象的资源,那么您需要移动语义(例如:您可以窃取 astd::unique_ptr指向的内容)。

移动语义允许您从函数返回不可复制的对象,这在当前标准中是不可能的。此外,不可复制的对象可以放在其他对象中,如果包含的对象是,这些对象将自动可移动。

不可复制的对象很棒,因为它们不会强迫您实现容易出错的复制构造函数。很多时候,复制语义并没有真正的意义,但移动语义确实有意义(想想看)。

std::vector<T>即使T是不可复制的,这也使您能够使用可移动类。在处理不可复制的对象(例如多态对象)时,std::unique_ptr类模板也是一个很好的工具。

于 2011-02-17T16:46:35.243 回答
11

经过一番挖掘,我在Stroustrup 的 FAQ中找到了这个使用右值引用进行优化的优秀示例。

是的,交换功能:

    template<class T> 
void swap(T& a, T& b)   // "perfect swap" (almost)
{
    T tmp = move(a);    // could invalidate a
    a = move(b);        // could invalidate b
    b = move(tmp);      // could invalidate tmp
}

这将为任何类型的类型生成优化代码(假设它具有移动构造函数)。

编辑: RVO 也不能优化这样的东西(至少在我的编译器上):

stuff func(const stuff& st)
{
    if(st.x>0)
    {
        stuff ret(2*st.x);
        return ret;
    }
    else
    {
        stuff ret2(-2*st.x);
        return ret2;
    }
}

这个函数总是调用复制构造函数(用 VC++ 检查)。如果我们的类可以移动得比使用移动构造函数更快,我们将进行优化。

于 2011-02-17T17:02:51.630 回答
7

想象一下,你的东西是一个像字符串一样具有堆分配内存的类,并且它具有容量的概念。给它一个 operator+= 它将以几何方式增长容量。在 C++03 中,这可能看起来像:

#include <iostream>
#include <algorithm>

struct stuff
{
    int size;
    int cap;

    stuff(int size_):size(size_)
    {
        cap = size;
        if (cap > 0)
            std::cout <<"allocating " << cap <<std::endl;
    }
    stuff(const stuff & g):size(g.size), cap(g.cap)
    {
        if (cap > 0)
            std::cout <<"allocating " << cap <<std::endl;
    }
    ~stuff()
    {
        if (cap > 0)
            std::cout << "deallocating " << cap << '\n';
    }

    stuff& operator+=(const stuff& y)
    {
        if (cap < size+y.size)
        {
            if (cap > 0)
                std::cout << "deallocating " << cap << '\n';
            cap = std::max(2*cap, size+y.size);
            std::cout <<"allocating " << cap <<std::endl;
        }
        size += y.size;
        return *this;
    }
};

stuff operator+(const stuff& lhs,const stuff& rhs)
{
    stuff g(lhs.size + rhs.size);
    return g;
}

还想象一下,您想一次添加的不仅仅是两个东西:

int main()
{
    stuff a(11),b(9),c(7),d(5);
    std::cout << "start addition\n\n";
    stuff e = a+b+c+d;
    std::cout << "\nend addition\n";
}

对我来说,这打印出来:

allocating 11
allocating 9
allocating 7
allocating 5
start addition

allocating 20
allocating 27
allocating 32
deallocating 27
deallocating 20

end addition
deallocating 32
deallocating 5
deallocating 7
deallocating 9
deallocating 11

我计算 3 个分配和 2 个释放来计算:

stuff e = a+b+c+d;

现在添加移动语义:

    stuff(stuff&& g):size(g.size), cap(g.cap)
    {
        g.cap = 0;
        g.size = 0;
    }

...

stuff operator+(stuff&& lhs,const stuff& rhs)
{
        return std::move(lhs += rhs);
}

再次运行我得到:

allocating 11
allocating 9
allocating 7
allocating 5
start addition

allocating 20
deallocating 20
allocating 40

end addition
deallocating 40
deallocating 5
deallocating 7
deallocating 9
deallocating 11

我现在减少到 2 个分配和 1 个释放。这转化为更快的代码。

于 2011-02-17T17:29:05.030 回答
4

有很多地方,其中一些在其他答案中提到。

一个重要的问题是,当调整 a 的大小时std::vector,它将移动感知对象从旧内存位置移动到新内存位置,而不是复制和破坏原始内存位置。

此外,右值引用允许可移动类型的概念,这是一种语义差异,而不仅仅是一种优化。unique_ptr在 C++03 中是不可能的,这就是为什么我们讨厌auto_ptr.

于 2011-02-17T19:00:58.673 回答
1

仅仅因为这个特殊情况已经被现有的优化所覆盖,并不意味着其他情况下不存在右值引用有用的情况。

即使从无法内联的函数(可能是虚拟调用或通过函数指针)返回临时值,移动构造也允许优化。

于 2011-02-17T16:45:05.597 回答
1

您发布的示例仅采用 const 左值引用,因此明确不能对其应用移动语义,因为那里没有单个右值引用。当您实现没有右值引用的类型时,移动语义如何使您的代码更快?

此外,您的代码已经被 RVO 和 NRVO 覆盖。移动语义适用于远比这两种情况多得多的情况。

于 2011-02-17T16:50:20.573 回答
0

此行调用第一个构造函数。

stuff a(5),b(7);

加号运算符是使用显式公共左值引用调用的。

stuff c = a + b;

在运算符重载方法中,您没有调用复制构造函数。同样,仅调用第一个构造函数。

stuff g(lhs.x+rhs.x);

分配是使用 RVO 进行的,因此不需要副本。不需要从返回的对象复制到“c”。

stuff c = a+b;

由于没有std::cout参考,编译器会注意你的c值永远不会被使用。然后,整个程序被剥离,导致一个空程序。

于 2013-09-07T21:33:27.730 回答
0

我能想到的另一个很好的例子。想象一下,您正在实现一个矩阵库并编写一个算法,该算法采用两个矩阵并输出另一个矩阵:

Matrix MyAlgorithm(Matrix U, Matrix V)
{
    Transform(U); //doesn't matter what this actually does, but it modifies U
    Transform(V);
    return U*V;
}

请注意,您不能通过 const 引用传递 U 和 V,因为算法会调整它们。从理论上讲,您可以通过引用传递它们,但这看起来很恶心并U处于V某种中间状态(因为您调用Transform(U)),这对调用者可能没有任何意义,或者根本没有任何数学意义,因为它只是一个内部算法转换。如果您只是按值传递它们,代码看起来更干净,如果您不打算使用U并且V在调用此函数后使用移动语义:

Matrix u, v;
...
Matrix w = MyAlgorithm(u, v); //slow, but will preserve u and v
Matrix w = MyAlgorithm(move(u), move(v)); //fast, but will nullify u and v
Matrix w = MyAlgorithm(u, move(v)); //and you can even do this if you need one but not the other
于 2017-11-14T18:01:11.620 回答