可以将本地堆栈变量 std::move 吗?

51
请考虑以下代码:
struct MyStruct
{
    int iInteger;
    string strString;
};

void MyFunc(vector<MyStruct>& vecStructs)
{
    MyStruct NewStruct = { 8, "Hello" };
    vecStructs.push_back(std::move(NewStruct));
}

int main()
{
    vector<MyStruct> vecStructs;
    MyFunc(vecStructs);
}

为什么这个工作?
在调用`MyFunc`的时候,返回地址应该被放置在当前线程的堆栈上。现在创建了`NewStruct`对象,它也应该被放置在堆栈上。通过使用`std::move`,我告诉编译器,我不打算再使用`NewStruct`的引用了。它可以窃取内存。(`push_back`函数是具有移动语义的函数。)
但是当函数返回并且`NewStruct`超出范围时,即使编译器不会从堆栈中删除原来存在的结构所占用的内存,它至少要删除之前存储的返回地址。
这将导致堆栈碎片化,未来的分配将覆盖“移动”的内存。
请有人给我解释一下这个问题吗?
编辑:
首先,非常感谢您的回答。
但是根据我所了解的,我仍然无法理解为什么以下内容不像我期望的那样工作:
struct MyStruct
{
    int iInteger;
    string strString;
    string strString2;
};

void MyFunc(vector<MyStruct>& vecStructs)
{
    MyStruct oNewStruct = { 8, "Hello", "Definetly more than 16 characters" };
    vecStructs.push_back(std::move(oNewStruct));

    // At this point, oNewStruct.String2 should be "", because its memory was stolen.
    // But only when I explicitly create a move-constructor in the form which was
    // stated by Yakk, it is really that case.
}

void main()
{
    vector<MyStruct> vecStructs;
    MyFunc(vecStructs);
}

3
除了main函数需要返回int之外,你的例子很好。移动构造函数只是将NewStruct状态移动到vecStructs中的新元素中。新元素与NewStruct是不同的,它们的生命周期没有任何联系。考虑使用std::vector::emplace_back代替push_back - François Andrieux
1
移动语义不会“删除”堆栈上的“插槽”。被移动的对象(例如您示例中的NewStruct)保证存在,尽管处于“未指定但可用状态”。 - el.pescado - нет войне
1
std::move 不会移动任何东西,它只是一个类型转换:https://dev59.com/emEi5IYBdhLWcg3wWLPc - doctorlove
“move” 更像是将一个银行账户的所有资金转移到另一个账户,而不是将一个银行账户转让给新的所有者。 - molbdnilo
2个回答

57

首先,std::move 并不会移动对象,而 std::forward 也并不会前向。

std::move 是将对象强制转换成右值引用。按照惯例,右值引用被视为“允许您从中移动数据的引用,调用者承诺他们确实不再需要该数据”。

另一方面,右值引用会隐式绑定到 std::move(有时是 forward)的返回值、临时对象,在从函数返回本地变量时的某些情况下以及在使用临时或已移动对象的成员时。

使用右值引用的函数内部发生的情况并不神奇。它不能直接声明所涉及对象的存储空间。但是,如果可以更快地执行操作,它可以撕掉其内部状态;按照惯例,它有权限(通过约定)更改其参数的内部状态。

现在,C++ 会自动为您编写一些移动构造函数。

struct MyStruct
{
  int iInteger;
  string strString;
};
在这种情况下,它将会写出一个大致看起来像这样的东西:
MyStruct::MyStruct( MyStruct&& other ) noexcept(true) :
  iInteger( std::move(other.iInteger) ),
  strString( std::move(other.strString) )
{}

换句话说,它将执行逐个元素的移动构造。

移动整数时,没有什么有趣的事情发生。没有任何好处来处理源整数的状态。

移动一个std::string,我们可以获得一些效率上的提升。C++标准描述了从一个std::string移动到另一个std::string时会发生什么。基本上,如果源std::string使用堆,则堆存储将传输到目标std::string

这是C++容器的一般模式;当你从它们中移动时,它们会窃取源容器的“堆分配”存储,并在目标中重复使用它。

请注意,源std::string仍然是std::string,只是其“内部”被摧毁了。大多数类似容器的东西都为空,我不记得std::string是否做出了那种保证(由于SBO可能不会),现在并不重要。

简而言之,当您移动某些内容时,它的内存不会“重新使用”,但是它所拥有的内存可以被重复使用。

在您的情况下,MyStruct具有一个使用堆分配内存的std::string。这个堆分配内存可以移动到存储在std::vector中的MyStruct中。

更进一步地深入研究,"Hello"可能如此短,以至于会发生SBO(小缓冲区优化),并且std::string根本不使用堆。对于这种特定情况,由于move,可能几乎没有性能提高。


@FrançoisAndrieux 这似乎是合理的;使用SBO,这使他们可以懒惰地在移动而不是复制时不必做额外的工作。我不打算进行标准深入探讨,因为这并不重要。 :) - Yakk - Adam Nevraumont
如果我理解正确的话,虽然std::move可以用于堆栈变量,但它的好处在于使用堆内存的类型。移动位于堆栈上的数组不会有任何作用,也不会对由原始类型构成的结构体产生影响。这是正确的吗? - Matthew Woo
2
@matt 对于基本类型的移动操作会进行复制。对于基本类型的聚合体(数组或结构体)的移动操作也会进行复制。然而,拥有两个基本数据副本的可观察效果非常少;因此编译器通常可以使用类似规则的方式省略它们的存在。最常见的问题是它们必须具有不同的地址,从而阻止省略;但是,超出范围的局部变量的地址是未定义的。对于 std 库中的组件(除了数组),移动操作可以避免堆重新分配。对于 std unique lock,移动是允许的,而复制则不允许。所以这是有所不同的。通常,资源所有类型 - Yakk - Adam Nevraumont
1
通过移动获取性能,因为它们可以避免重复使用资源(无论是在自由存储器还是其他地方);在其他情况下,移动允许语义操作,而复制则不允许(比较auto ptr和unique ptr、unique lock或shared lock;现代类型基于语义原因阻止复制)。 - Yakk - Adam Nevraumont

29

你的例子可以简化为:

vector<string> vec;
string str; // populate with a really long string
vec.push_back(std::move(str));
这仍然引发了一个问题,“是否可以移动本地堆栈变量。” 它只是删除了一些不必要的代码,使其更易于理解。
答案是肯定的。像上面的代码一样,可以受益于std :: move,因为std :: string - 至少如果内容足够大 - 将其实际数据存储在堆上,即使变量位于堆栈上。
如果您不使用std :: move(),则可以预期像上面的代码一样复制str的内容,该内容可能是任意大的。如果您使用std :: move(),则仅会复制字符串的直接成员(移动不需要“清零”旧位置),并且数据将在不修改或复制的情况下使用。
这基本上是这两者之间的区别:
char* str; // populate with a really long string
char* other = new char[strlen(str)+1];
strcpy(other, str);

VS

char* str; // populate with a really long string
char* other = str;

在这两种情况下,变量都位于堆栈上。但是数据不是。

如果您有一种情况,其中真正所有的数据都在堆栈上,例如启用了“小字符串优化”的std::string或包含整数的结构体,则使用std::move()将不会带来任何好处。


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