从栈移动到堆
Move from stack to heap
我正在尝试调用一些函数,其中包括向向量添加元素(按值传递的参数):
std::vector<Val> vec;
void fun( Val v )
{
...
vec.push_back(std::move( v ));
...
}
问题是:移动语义有什么好处吗?
当新的向量元素构造时,我对变量 "moved" 从堆栈到堆感到困惑:它似乎只是被复制。
如果std::move允许底层对象将指针从一个移动到另一个;而不是(在复制中完成的)分配一个新区域,然后复制可能是大量内存的内容。例如
class derp {
int* data;
int dataSize;
}
数据和数据大小可以在移动中交换;但是副本可能要贵得多
但是,如果您只有一个整数集合;移动和复制将等同于同一件事;除了旧版本的对象应该失效;无论在该对象的上下文中应该意味着什么。
移动而不是复制是否有任何好处实际上取决于Val
的实施。
例如,如果Val
的复制构造函数的时间复杂度为O(n),但其移动构造函数的复杂度为O(1)(例如:它只包含更改一些内部指针),那么可能会有性能优势。
在 Val
的以下实现中,移动而不是复制不会带来任何性能优势:
class Val {
int data[1000];
...
};
但下面的可以:
class Val {
int *data;
size_t num;
...
};
移动是对对象内部存储的盗窃(如果适用)。许多标准容器可以从中移动,因为它们在堆上分配存储空间,这对 "move" 来说是微不足道的。在内部,会发生如下情况。
template<class T>
class DumbContainer {
private:
size_t size;
T* buffer;
public:
DumbContainer(DumbContainer&& other) {
buffer = other.buffer;
other.buffer = nullptr;
}
// Constructors, Destructors and member functions
}
如您所见,对象在存储中没有移动,"move" 纯粹是从容器 other
到 this
的概念。事实上,这是优化的唯一原因是对象保持不变。
堆栈上的存储无法移动到另一个容器,因为堆栈上的生命周期与当前范围相关联。例如,std::array<T>
将无法将其内部缓冲区移动到另一个数组,但如果 T
在堆上有存储空间可以传递,从中移动仍然很有效。例如,从 std::array<std::vector<T>>
移动将不得不构造 vector
对象(array
不可移动),但是 vector
将廉价地将其管理的对象移动到 vector
新创建的 array
.
所以在你的例子中,如果 Val
是一个可移动的对象,像下面这样的函数可以减少开销。
std::vector<Val> vec;
void fun( Val v )
{
...
vec.emplace_back(std::move( v ));
...
}
您无法摆脱构建 vector
、在容量已满时分配额外的 space 或在 vector
中构建 Val
对象,但是如果 Val
只是一个指向堆存储的指针,那么在不实际窃取另一个向量的情况下,它是尽可能便宜的。如果 Val
不可移动,则将其转换为右值不会丢失任何内容。这种类型的函数签名的好处是调用者可以决定他们是想要复制还是移动。
// Consider the difference of these calls
Val v;
fun(v);
fun(std::move v);
第一个将调用 Val
的复制构造函数(如果存在),否则将无法编译。如果存在,第二个将调用移动构造函数,否则调用复制构造函数。如果两者都不存在,它将不会编译。
我正在尝试调用一些函数,其中包括向向量添加元素(按值传递的参数):
std::vector<Val> vec;
void fun( Val v )
{
...
vec.push_back(std::move( v ));
...
}
问题是:移动语义有什么好处吗?
当新的向量元素构造时,我对变量 "moved" 从堆栈到堆感到困惑:它似乎只是被复制。
如果std::move允许底层对象将指针从一个移动到另一个;而不是(在复制中完成的)分配一个新区域,然后复制可能是大量内存的内容。例如
class derp {
int* data;
int dataSize;
}
数据和数据大小可以在移动中交换;但是副本可能要贵得多
但是,如果您只有一个整数集合;移动和复制将等同于同一件事;除了旧版本的对象应该失效;无论在该对象的上下文中应该意味着什么。
移动而不是复制是否有任何好处实际上取决于Val
的实施。
例如,如果Val
的复制构造函数的时间复杂度为O(n),但其移动构造函数的复杂度为O(1)(例如:它只包含更改一些内部指针),那么可能会有性能优势。
在 Val
的以下实现中,移动而不是复制不会带来任何性能优势:
class Val {
int data[1000];
...
};
但下面的可以:
class Val {
int *data;
size_t num;
...
};
移动是对对象内部存储的盗窃(如果适用)。许多标准容器可以从中移动,因为它们在堆上分配存储空间,这对 "move" 来说是微不足道的。在内部,会发生如下情况。
template<class T>
class DumbContainer {
private:
size_t size;
T* buffer;
public:
DumbContainer(DumbContainer&& other) {
buffer = other.buffer;
other.buffer = nullptr;
}
// Constructors, Destructors and member functions
}
如您所见,对象在存储中没有移动,"move" 纯粹是从容器 other
到 this
的概念。事实上,这是优化的唯一原因是对象保持不变。
堆栈上的存储无法移动到另一个容器,因为堆栈上的生命周期与当前范围相关联。例如,std::array<T>
将无法将其内部缓冲区移动到另一个数组,但如果 T
在堆上有存储空间可以传递,从中移动仍然很有效。例如,从 std::array<std::vector<T>>
移动将不得不构造 vector
对象(array
不可移动),但是 vector
将廉价地将其管理的对象移动到 vector
新创建的 array
.
所以在你的例子中,如果 Val
是一个可移动的对象,像下面这样的函数可以减少开销。
std::vector<Val> vec;
void fun( Val v )
{
...
vec.emplace_back(std::move( v ));
...
}
您无法摆脱构建 vector
、在容量已满时分配额外的 space 或在 vector
中构建 Val
对象,但是如果 Val
只是一个指向堆存储的指针,那么在不实际窃取另一个向量的情况下,它是尽可能便宜的。如果 Val
不可移动,则将其转换为右值不会丢失任何内容。这种类型的函数签名的好处是调用者可以决定他们是想要复制还是移动。
// Consider the difference of these calls
Val v;
fun(v);
fun(std::move v);
第一个将调用 Val
的复制构造函数(如果存在),否则将无法编译。如果存在,第二个将调用移动构造函数,否则调用复制构造函数。如果两者都不存在,它将不会编译。