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


655
  1. 它是什么?
  2. 它有什么作用?
  3. 什么时候应该使用?

良好的链接表示赞赏。


42
Bjarne Stroustrup在“右值引用简介
DumbCoder 2010年


12
这个问题是指std::move(T && t); 还有std::move(InputIt first, InputIt last, OutputIt d_first)一个与关联的算法std::copy。我指出,这样的话,其他人不会像我第一次面对std::move三个论点时那样困惑。cn.cppreference.com/w/cpp/algorithm/move
josaphatv

Answers:


285

Wikipedia页面上的C ++ 11 R值引用和移动构造函数

  1. 在C ++ 11中,除了复制构造函数之外,对象还可以具有move构造函数。
    (除了复制分配运算符,他们还有移动分配运算符。)
  2. 如果对象的类型为“ rvalue-reference”(Type &&),则使用move构造函数而不是copy构造函数。
  3. std::move() 是一种产生对对象的右值引用以允许从其移动的演员表。

这是避免复制的新C ++方法。例如,使用move构造函数,a std::vector可以将其内部指针复制到数据中,并将其复制到新对象,而使移动后的对象处于“ from from”状态,因此不复制所有数据。这将是C ++有效的。

尝试谷歌搜索以获取移动语义,右值,完美转发。


39
移动语义要求被移动的对象保持有效,这不是错误的状态。(合理性:它仍然必须进行破坏,才能发挥作用。)
GManNickG

13
@GMan:好吧,它必须处于可以安全销毁的状态,但是,AFAIK,它不必用于其他任何用途。
Zan Lynx

8
@ZanLynx:对。请注意,标准库还要求可移动对象是可分配的,但这仅适用于stdlib中使用的对象,不是一般要求。
GManNickG 2011年

24
-1 “ std :: move()是C ++ 11使用移动语义的方法”,请解决此问题。std::move()不是使用移动语义的方法,移动语义对程序员透明地执行。move它只是将值从一个点传递到另一点的转换,而原来的左值将不再使用。
Manu343726

15
我走得更远。std::move本身不执行任何操作-副作用为零。它只是向编译器发出信号,表明程序员不再关心该对象发生了什么。也就是说,它允许软件的其他部分从对象中移出,但不需要移动它。实际上,右值引用的接收者不必对它将对数据做什么或不做任何保证。
亚伦·麦克戴德

240

1.“这是什么?”

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

2.“它是做什么的?”

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

您可以掠夺我,我持有的任何物品转移到其他地方使用(因为无论如何我都会很快被销毁)”。

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

您也可以从prvalue(例如您要经过的临时地址)移开,但这很少有用。

3.“何时应使用?”

提出此问题的另一种方式是“我将蚕食现有对象的资源用于什么?” 好吧,如果您正在编写应用程序代码,则可能不会对由编译器创建的临时对象产生太多困扰。因此,主要是在诸如构造函数,运算符方法,类似于标准库算法的函数等地方执行此操作,在该处对象会被大量自动创建和销毁。当然,这只是一个经验法则。

一种典型的用法是将资源从一个对象“移动”到另一个对象,而不是复制。@Guillaume链接到此页面,它有一个简单的简短示例:交换两个对象且复制较少。

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ 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是,比如说,vector<int>大小为n。在第一个版本中,您读写3 * n个元素,在第二个版本中,您基本上只读写指向向量缓冲区的3个指针,再加上3个缓冲区的大小。当然,上课T需要知道如何去做。您的班级应该有一个班级分配操作符和一个班机构造函数T,此类才能起作用。


3
很长一段时间以来,我都听说过这些移动语义,因此从未研究过它们。通过此描述,您已经给出了它似乎是一个浅表副本,而不是深表副本。
Zebrafish

7
@TitoneMaurice:除了它不是副本外,因为原始值不再可用。
einpoklum

3
@Zebrafish,您再认错了。浅拷贝使原件保持完全相同的状态,移动通常会导致原件为空或处于其他有效状态。
rubenvb

16
@rubenvb Zebra并非完全错误。的确,通常会故意破坏原始的对象以避免混乱的错误(例如,将其指针设置为nullptr以表示其不再拥有指针),但整个动作是通过简单地从源中复制指针来实现的确实到达目的地(并故意避免与pointe做任何事情)确实让人联想到浅表副本。实际上,我什至可以说一个举动一个浅表副本,然后可选地是对源进行部分自毁。(续)
轻轨赛将于

3
(续)如果我们允许这个定义(我更喜欢它),那么@Zebrafish的观察是正确的,只是有些不完整。
Lightness Races in Orbit

145

当需要将对象的内容“转移”到其他地方而不进行复制时,可以使用move来进行复制(即,内容不重复,这就是为什么可以将其用于某些不可复制的对象(例如unique_ptr)的原因)。使用std :: move,对象也可以不进行复制而获取临时对象的内容(并节省大量时间)。

这个链接确实帮助了我:

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

很抱歉,我的答案来不及了,但是我也在寻找std :: move的一个很好的链接,我发现上面的链接有点“严肃”。

这将重点放在r值引用上,在这种情况下您应该使用它们,而且我认为它更详细,这就是为什么我想在这里共享此链接。


26
好的链接。我总是会发现维基百科的文章,以及我偶然发现的其他链接,因为它们只是向您抛出事实,让您弄清楚实际含义/意义是什么,因此我感到很困惑。尽管构造函数中的“移动语义”非常明显,但是关于传递&&-value的所有这些细节都不是...,因此教程风格的描述非常好。
Christian Stieber

66

问:什么std::move

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

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

static_cast<T&&>(t);

右值是一个临时值,不会超出定义它的表达式,例如临时存储在变量中的中间函数结果。

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:“右值引用简介”中给出了std :: move()的实现,如下所示:

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

如您所见,无论是使用值(),引用类型()还是右值引用()调用,都std::move将返回。T&&TT&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)`

它能做什么不能做的:

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

问:什么时候应该使用?

答:如果要使用std::move非右值(临时表达式)的参数调用支持移动语义的函数,则应使用。

这为我提出了以下后续问题:

  • 什么是移动语义?与复制语义相反,移动语义是一种编程技术,其中对象的成员通过“接管”而不是复制另一个对象的成员来初始化。这样的“接管”仅对于指针和资源句柄才有意义,可以通过复制指针或整数句柄而不是底层数据来廉价地进行传递。

  • 什么样的类和对象支持移动语义?如果开发语义可以从成员转移而不是复制成员中受益,则由您自己决定如何在自己的类中实现移动语义。一旦实现了移动语义,您将直接从许多图书馆程序员的工作中受益,这些图书馆程序员增加了对有效处理具有移动语义的类的支持。

  • 为何编译器无法自行解决?除非您这样说,否则编译器不能仅调用函数的另一个重载。您必须帮助编译器选择应调用函数的常规版本还是移动版本。

  • 在哪种情况下,我想告诉编译器应将变量视为右值?这很可能会在模板或库函数中发生,您知道可以挽救中间结果。


2
带有注释语义的代码示例的大+1。其他最重要的答案使用“ move”本身定义了std :: move-并没有真正弄清楚什么!---我认为值得一提的是,不复制该参数就意味着不能可靠地使用原始值。
ty

34

std :: move本身并没有做太多事情。我以为它调用了对象的移动构造函数,但实际上只是执行类型转换(将左值变量投射到右值,以便可以将所述变量作为参数传递给移动构造函数或赋值运算符)。

因此std :: move仅用作使用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。其余部分使用move语义。还需要实现move构造函数或赋值运算符,才能将结果分配给A。


3
您谈到了移动语义。您应该将std :: move的用法添加到您的答案中,因为问题询问了这一点。
Koushik Shetty

2
@Koushik std :: move并没有做太多的事情-但是用于实现移动语义。如果您不了解std :: move,那么您也可能也不知道移动语义
user929404 2013年

1
“不做很多事情”(仅是对右值引用的static_cast)。实际操作是什么,y是操作员要求的。您无需了解std :: move的工作原理,但您必须了解move语义的作用。此外,“相反,它却被用来实现移动语义”。知道移动语义,您将了解std :: move,否则不行。移动只是有助于移动,它本身使用移动语义。std :: move除了将其参数转换为右值引用外,什么也不做,这是移动语义所要求的。
库希克·谢蒂

10
“但是E + F会产生一个临时对象”-运算符+从左到右,而不是从右到左。因此B+C将是第一!
2015年

8

“它是什么?” “它是做什么的?” 上面已经解释了。

我将举例说明“何时使用”。

例如,我们有一个类,其中包含很多资源,例如大数组。

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通过with move constructor可以轻松地转换资源。

还有什么地方std::move有用?

std::move在对元素数组进行排序时也很有用。许多排序算法(例如选择排序和冒泡排序)通过交换元素对来工作。以前,我们不得不诉诸于复制语义来进行交换。现在,我们可以使用更有效的移动语义。

如果我们想将一个智能指针管理的内容移动到另一个智能指针,则它也很有用。

引用:


0

这是一个完整的示例,使用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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.