std::move()是什么,以及何时应该使用它?

1102
1. 这是什么? 2. 它有什么作用? 3. 什么时候应该使用它?
感谢提供好的链接。

62
Bjarne Stroustrup在《Rvalue引用简介》中解释了移动语义。 - DumbCoder
2
移动语义 - Basilevs
21
这个问题是关于std::move(T && t)的;还有一个与std::copy相关的算法,即std::move(InputIt first, InputIt last, OutputIt d_first)。我提醒一下其他人,以免像我第一次遇到接受三个参数的std::move时感到困惑。具体详情请查看http://en.cppreference.com/w/cpp/algorithm/move。 - josaphatv
1
如果您不太了解左值和右值引用的含义,建议阅读以下内容:https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c - Karan Singh
1
请注意,这个问题是关于来自<utility>std::move,而不是来自<algorithm>std::move - Adrian McCarthy
9个回答

570

1. "这是什么?"

虽然 std::move() 技术上是一个函数,但我会说它并不是真正的函数。它是一种在编译器考虑表达式值的方式之间的转换器

2. "它是做什么的?"

首先要注意的是,std::move() 实际上并没有移动任何东西。它将一个表达式从lvalue(例如命名变量)更改为xvalue。xvalue 告诉编译器:

你可以掠夺我,移动我持有的任何东西并在其他地方使用它(因为我很快就会被销毁)。

换句话说,当你使用std::move(x)时,你允许编译器吞噬x。因此,如果x在内存中有自己的缓冲区,那么在std::move()之后,编译器可以让另一个对象拥有它。

你也可以从prvalue(例如你正在传递的临时对象)中移动,但这很少有用。

3. "什么时候应该使用?"

另一个问这个问题的方式是“我为什么要吞噬现有对象的资源?”好吧,如果你正在编写应用程序代码,你可能不会经常处理由编译器创建的临时对象。因此,主要是在构造函数、运算符方法、类似于标准库算法的函数等地方进行操作,其中对象会自动创建和销毁。当然,这只是一个经验法则。

一个典型的用途是将资源从一个对象移动到另一个对象,而不是复制。@Guillaume链接到此页面,其中包含一个简单明了的短例子:使用较少的复制交换两个对象。
template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we've made a second copy of a
    a = b;      // we've made a second copy of b (and discarded a copy of a)
    b = tmp;    // we've made a second copy of tmp (and discarded a copy of b)
}

使用move可以交换资源而非复制它们:
template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

想象一下当T是大小为n的vector<int>时会发生什么。在第一个版本中,您将读取和写入3 * n个元素,在第二个版本中,您基本上只读取和写入指向向量缓冲区的3个指针,以及3个缓冲区的大小。当然,类T需要知道如何进行移动; 你的类应该为此拥有一个移动赋值运算符和一个移动构造函数来处理类T


21
我听说过移动语义很长一段时间了,但从未深入了解过。根据你给出的描述,它似乎只是浅复制而不是深复制。 - Zebrafish
44
除了这个原值不再可用以外,它并不是一份复制品。 - einpoklum
14
@Zebrafish你错得不能再错了。浅复制不改变原始数据,而拷贝通常会导致原始数据为空或进入其他有效状态。 - rubenvb
90
@rubenvb并不完全错误。虽然原始的被“ cannabilised ”的对象通常会被有意地破坏以避免混淆错误(例如将其指针设置为nullptr以表示它不再拥有指针对应的对象),但整个移动操作实际上只是通过从源到目标简单地复制一个指针来实现的(并故意避免对指针所指向的对象进行任何操作),这确实让人想起浅复制。事实上,我甚至可以说,一个移动就是一个浅复制,然后是源的部分自毁。(续) - Lightness Races in Orbit
28
如果我们采用这个定义(我很喜欢它),那么@Zebrafish的观察并不是错误的,只是稍微不完整。 - Lightness Races in Orbit
显示剩余5条评论

419

C++11中关于右值引用和移动构造函数的维基百科页面

  1. C++11中,除了拷贝构造函数外,对象还可以具有移动构造函数。(同样,除了拷贝赋值操作符,它们还有移动赋值操作符。)
  2. 如果对象具有类型“右值引用”(即Type &&),则使用移动构造函数代替拷贝构造函数。
  3. std::move()是一种类型转换,它产生一个指向对象的右值引用,以便从中移动。

这是C++的一种新方式,用于避免拷贝。例如,通过使用移动构造函数,std::vector只需将其内部数据指针复制到新对象中,将移动后的对象保留在已移动状态,从而避免拷贝所有数据。这是符合C++标准的。

尝试搜索“移动语义”,“右值”,“完美转发”等相关内容。


47
移动语义要求被移动的对象保持有效状态,这并不是一个不正确的状态。(理由:它仍然需要被析构,让它工作。) - GManNickG
19
@GMan:好的,它必须处于安全摧毁状态,但据我所知,它不必用于其他任何事情。 翻译:需要处于安全拆除状态,但据我所知,无需满足其他用途。 - Zan Lynx
9
@ZanLynx:没错。请注意,标准库还要求移动对象可分配,但这仅适用于在stdlib中使用的对象,而不是一般要求。 - GManNickG
34
“std::move() 是C++11中使用移动语义的方法”这句话需要修改。“std::move()”并不是使用移动语义的方式,移动语义是对程序员透明的。"move"仅仅是一种转换,用于将一个值从一个位置传递到另一个位置,原始的左值将不再被使用。请修正该表述。 - Manu343726
30
我想进一步说明。std::move 本身并不会做任何事情 - 它没有任何副作用。它只是向编译器发出信号,告诉程序员不再关心该对象的任何情况。也就是说,它允许软件的其他部分从该对象中移动,但并不要求移动。实际上,rvalue 引用的接收者不必对其将要或不将要做什么承诺。 - Aaron McDaid
显示剩余6条评论

170

当你需要将对象的内容转移到其他地方而不进行复制(即不重复内容,这就是为什么它可以用于一些不可复制的对象,如unique_ptr)时,可以使用move。此外,还可以使用std::move使对象获取临时对象的内容而不进行复制(并节省大量时间)。

这个链接真的帮了我很多:

http://thbecker.net/articles/rvalue_references/section_01.html

如果我的回答来得太晚了,我感到很抱歉,但我也在寻找std::move的好链接,我发现上面的链接有点“简陋”。

这强调了r值引用,在这种情况下应该如何使用它们,我认为它更详细,这就是为什么我想在这里分享这个链接。


30
好链接。我总是觉得维基百科文章和其他我偶然发现的链接很令人困惑,因为它们只是向你提供事实,让你自己想出实际的意义/理由。虽然在构造函数中使用"move semantics"相当明显,但是关于传递&&-值的所有细节并不明显......所以这种教程式的描述非常好。 - Christian Stieber

141

问题:什么是std::move

答案: std::move() 是 C++ 标准库中的一个函数,用于将对象转换为右值引用。

简单来说,std::move(t) 相当于:

static_cast<T&&>(t);

一个rvalue是一个临时值,它不会在定义它的表达式之外持续存在,例如一个中间函数结果,它从未存储在变量中。
int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

N2027: "Rvalue引用简介"中,给出了std::move()的实现如下:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

如您所见,无论是使用值(T)、引用类型(T&)还是右值引用(T&&),std::move 都会返回 T&&

问:它有什么作用?

答:作为一种转换方式,它在运行时不会执行任何操作。它只是在编译时起作用,告诉编译器您希望将引用视为右值继续处理。

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

会做以下事情:

  • 复制参数
  • 调用复制构造函数
  • 更改参数对象

Q:何时应该使用它?

A:如果您想要使用不是rvalue(临时表达式)的参数调用支持移动语义的函数,则应使用std::move

这让我有以下后续问题:

  • 什么是移动语义?与复制语义相比,移动语义是一种编程技术,通过“接管”而不是复制另一个对象的成员来初始化对象的成员。此类“接管”仅在指针和资源句柄方面有意义,可以通过复制指针或整数句柄而不是基础数据轻松转移。

  • 哪些类和对象支持移动语义?如果您的类从传输其成员而不是复制它们中获益,则作为开发人员实现移动语义取决于您自己。一旦实现了移动语义,您将直接受益于许多库程序员添加了支持有效处理具有移动语义的类。

  • 为什么编译器无法自行解决问题?除非您明确指出,否则编译器无法调用函数的另一个重载。您必须帮助编译器选择调用正常或移动版本的函数。

  • 在哪些情况下我想告诉编译器它应将变量视为rvalue?这很可能发生在模板或库函数中,在那里您知道可以挽救中间结果(而不是分配新实例)。


13
在代码示例中加入语义注释,大力支持。其他的最佳答案使用“move”来定义std :: move - 这并没有真正澄清任何问题! --- 我认为值得一提的是,不复制参数意味着原始值无法可靠地使用。 - llf

47

std::move本身实际上并没有太多作用。我曾以为它会调用对象的移动构造函数,但实际上它只是执行类型转换(将lvalue变量强制转换为rvalue,以便将该变量作为参数传递给移动构造函数或赋值运算符)。

因此,std::move只是在使用移动语义之前使用的先决条件。移动语义本质上是一种处理临时对象的高效方法。

考虑对象 A = B + (C + (D + (E + F)));

这是很好看的代码,但 E + F 会产生一个临时对象。然后 D + temp 会产生另一个临时对象,以此类推。在类的每个普通“+”运算符中,都会发生深度复制。

例如

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

这个函数中的临时对象创建是无用的——因为它们会在行末超出作用域而被删除。

我们可以使用移动语义来“掠夺”这些临时对象,做一些像下面这样的事情

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

这样可以避免进行不必要的深拷贝。就这个例子而言,现在唯一进行深复制的部分是E + F。其余部分使用移动语义。移动构造函数或赋值运算符还需要被实现以将结果分配给A。


3
你谈到了移动语义。你应该在回答中补充说明如何使用std::move,因为问题是关于这个的。 - Koushik Shetty
2
@Koushik std::move并没有太多作用,但是它被用来实现移动语义。如果你不了解std::move,那么你可能也不了解移动语义。 - user929404
1
“不做太多事情”(只是一个 static_cast 到右值引用)。它实际上做了什么以及为什么这样做是 OP 所问的。你不需要知道 std::move 如何工作,但你必须知道移动语义的作用。此外,“但用于实现移动语义”是相反的。了解移动语义,你就会理解 std::move,否则不会。移动只是帮助移动,并且本身使用移动语义。std::move 除了将其参数转换为右值引用外,什么也不做,这正是移动语义所需的。 - Koushik Shetty
11
运算符+是从左到右结合的,而不是从右到左。因此,先计算B+C!但是E+F会产生一个临时对象。 - Ajay
@Ajay:B+C不能放在第一位,要看括号中指定的预期评估顺序和语义,其中E+F应该首先进行评估并创建一个临时变量,除非另有规定,否则不具有移动语义(这个临时总和称为“temp”是一个rvalue,而不是lvalue),这意味着在评估F+temp时,运算符应该首先取一个lvalue(F)和第二个rvalue(temp),因此它不一定是相同的重载“+”运算符。如果您没有指定移动语义,则编译器可能会考虑从左到右进行评估,而不选择正确的运算符。 - verdy_p
显示剩余4条评论

13

上面已经解释了"它是什么?""它是做什么的?"

我将举一个例子来"何时应该使用它"

例如,我们有一个包含大量资源(如大数组)的类。

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

测试代码:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

以下是输出结果:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

我们可以看到,使用具有移动构造函数的std::move可以轻松转换资源。
在哪些其他地方可以使用std::move呢?
当对元素数组进行排序时,std::move也很有用。许多排序算法(例如选择排序和冒泡排序)通过交换一对元素来工作。以前,我们不得不使用复制语义来进行交换。现在,我们可以使用移动语义,这更加高效。
如果我们想将一个智能指针管理的内容移动到另一个智能指针中,std::move也很有用。
引用:

2
std::move本身并不执行任何操作,只是一个static_cast。根据cppreference.com的说法:

它等价于将其转换为右值引用类型的static_cast

因此,它取决于您在move之后分配给的变量的类型,如果该类型具有构造函数赋值运算符,可以接受一个右值参数,则它可能会夺取原始变量的内容,因此,可能会使原始变量处于未指定状态

除非另有说明,所有已移动的标准库对象都被放置在有效但未指定状态中。

由于内置字面类型(如整数和原始指针)没有特殊的移动构造函数移动赋值运算符,所以对于这些类型,它只是一个简单的复制。

-1

这里是一个完整的示例,使用std::move来处理(简单的)自定义向量

预期输出:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

编译为:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

代码:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}

-1

std::move 简单地将一个变量转换为右值引用。这个右值引用用 && 表示。假设你有一个类 Foo,你可以像这样实例化一个对象

Foo foo = Foo();

如果你接下来写

Foo foo2 = std::move(foo);

这与我写的一样

Foo foo2 = (Foo&&) foo;

std::move 用于替换将对象转换为右值引用的强制类型转换。 你想要编写前两行代码的原因是,如果你编写

Foo foo2 = foo;

将调用复制构造函数。 假设Foo实例具有指向堆上某些数据的指针,它们拥有这些数据。 在Foo的析构函数中,堆上的数据被删除。 如果您想区分从堆中复制数据和拥有该数据的所有权,则可以编写一个构造函数,该构造函数接受const Foo&,并且该构造函数可以执行深度复制。然后,您可以编写一个构造函数,该构造函数接受rvalue引用(Foo&&),并且此构造函数可以简单地重新连接指针。 当您编写时,将调用此接受Foo&&的构造函数

Foo foo2 = std::move(foo);

当你写代码时

Foo foo2 = (Foo&&) foo;

我认为使用 C 风格转换 "(Foo&&)" 会产生误导,因为与使用 @ChristopherOezbeck 已经建议的 static_cast 相比,它提供很少的编译时保证。更多信息请参见这里。否则,这个答案并没有添加太多已经被涵盖的东西。 - alexpanter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接