感谢提供好的链接。
虽然 std::move()
技术上是一个函数,但我会说它并不是真正的函数。它是一种在编译器考虑表达式值的方式之间的转换器。
首先要注意的是,std::move()
实际上并没有移动任何东西。它将一个表达式从lvalue(例如命名变量)更改为xvalue。xvalue 告诉编译器:
你可以掠夺我,移动我持有的任何东西并在其他地方使用它(因为我很快就会被销毁)。
换句话说,当你使用std::move(x)
时,你允许编译器吞噬x
。因此,如果x
在内存中有自己的缓冲区,那么在std::move()
之后,编译器可以让另一个对象拥有它。
你也可以从prvalue(例如你正在传递的临时对象)中移动,但这很少有用。
另一个问这个问题的方式是“我为什么要吞噬现有对象的资源?”好吧,如果你正在编写应用程序代码,你可能不会经常处理由编译器创建的临时对象。因此,主要是在构造函数、运算符方法、类似于标准库算法的函数等地方进行操作,其中对象会自动创建和销毁。当然,这只是一个经验法则。
一个典型的用途是将资源从一个对象移动到另一个对象,而不是复制。@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)
}
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
。
Type &&
),则使用移动构造函数代替拷贝构造函数。std::move()
是一种类型转换,它产生一个指向对象的右值引用,以便从中移动。这是C++的一种新方式,用于避免拷贝。例如,通过使用移动构造函数,std::vector
只需将其内部数据指针复制到新对象中,将移动后的对象保留在已移动状态,从而避免拷贝所有数据。这是符合C++标准的。
尝试搜索“移动语义”,“右值”,“完美转发”等相关内容。
std::move
本身并不会做任何事情 - 它没有任何副作用。它只是向编译器发出信号,告诉程序员不再关心该对象的任何情况。也就是说,它允许软件的其他部分从该对象中移动,但并不要求移动。实际上,rvalue 引用的接收者不必对其将要或不将要做什么承诺。 - Aaron McDaid当你需要将对象的内容转移到其他地方而不进行复制(即不重复内容,这就是为什么它可以用于一些不可复制的对象,如unique_ptr)时,可以使用move。此外,还可以使用std::move使对象获取临时对象的内容而不进行复制(并节省大量时间)。
这个链接真的帮了我很多:
http://thbecker.net/articles/rvalue_references/section_01.html
如果我的回答来得太晚了,我感到很抱歉,但我也在寻找std::move的好链接,我发现上面的链接有点“简陋”。
这强调了r值引用,在这种情况下应该如何使用它们,我认为它更详细,这就是为什么我想在这里分享这个链接。
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: "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)`
它不会做以下事情:
A:如果您想要使用不是rvalue(临时表达式)的参数调用支持移动语义的函数,则应使用std::move
。
这让我有以下后续问题:
什么是移动语义?与复制语义相比,移动语义是一种编程技术,通过“接管”而不是复制另一个对象的成员来初始化对象的成员。此类“接管”仅在指针和资源句柄方面有意义,可以通过复制指针或整数句柄而不是基础数据轻松转移。
哪些类和对象支持移动语义?如果您的类从传输其成员而不是复制它们中获益,则作为开发人员实现移动语义取决于您自己。一旦实现了移动语义,您将直接受益于许多库程序员添加了支持有效处理具有移动语义的类。
为什么编译器无法自行解决问题?除非您明确指出,否则编译器无法调用函数的另一个重载。您必须帮助编译器选择调用正常或移动版本的函数。
在哪些情况下我想告诉编译器它应将变量视为rvalue?这很可能发生在模板或库函数中,在那里您知道可以挽救中间结果(而不是分配新实例)。
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。
+
是从左到右结合的,而不是从右到左。因此,先计算B+C
!但是E+F
会产生一个临时对象。 - Ajay上面已经解释了"它是什么?"和"它是做什么的?"
我将举一个例子来"何时应该使用它"
例如,我们有一个包含大量资源(如大数组)的类。
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
也很有用。std::move
本身并不执行任何操作,只是一个static_cast
。根据cppreference.com的说法:
因此,它取决于您在它等价于将其转换为右值引用类型的
static_cast
。
move
之后分配给的变量的类型,如果该类型具有构造函数
或赋值运算符
,可以接受一个右值参数,则它可能会夺取原始变量的内容,因此,可能会使原始变量处于未指定状态
:
由于内置字面类型(如整数和原始指针)没有特殊的除非另有说明,所有已移动的标准库对象都被放置在有效但未指定状态中。
移动构造函数
或移动赋值运算符
,所以对于这些类型,它只是一个简单的复制。这里是一个完整的示例,使用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;
}
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;
(Foo&&)
" 会产生误导,因为与使用 @ChristopherOezbeck 已经建议的 static_cast
相比,它提供很少的编译时保证。更多信息请参见这里。否则,这个答案并没有添加太多已经被涵盖的东西。 - alexpanter
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<utility>
的std::move
,而不是来自<algorithm>
的std::move
。 - Adrian McCarthy