使用局部变量移动语义

move semantics with local variables

我想知道从这个例子中提取的以下代码:

Is it possible to std::move local stack variables?

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);
}

我了解 std::move() 在基本层面上的工作原理,但背后有些东西对我来说没有意义。

例如,如果 NewStruct 是在堆栈上本地创建的,如示例中所示。如果我在退出函数范围之前使用移动语义来保存它的数据不被销毁,那么 NewStruct 被销毁不会影响我的数据,这很好。但是这些信息不还是放在栈上吗?如果我要再次扩展我对堆栈的使用,为什么一旦堆栈增长并且想要覆盖 NewStruct 最初创建数据的位置,为什么这些信息不会有被覆盖的危险?

添加第二个示例可能会更好地表达我的观点:

    void MyFunc(vector<char*> &vecCharPointers)
    {
        char* myStr = {'H', 'e', 'l', 'l', 'o'};
        vecCharPointers.push_back(std::move(myStr));
    }

    int main()
    {
        vector<char*> vecCharPointers;
        char* cp = nullptr;
    }

移动语义对对象的范围没有影响。在此示例中,NewStruct 具有自动作用域,因此它一直在作用域中,直到它在其中声明的函数结束。

移动 NewStruct 的内容没有任何区别。 NewStruct的作用域仍然是自动的,只有在函数returns.

时才会被销毁

在同一个函数中,在自动范围内声明额外的对象,与 NewStruct 的内容没有被移动到任何地方具有相同的实际效果。

“移动”一个物体并不像你想象的那样。这并不意味着“这个物体在一瞬间消失在一阵烟雾中”,并且从那时起不再存在。在移动之后,它仍然处于某种有效但未指定的状态。移动语义基本上意味着你在告诉你的 C++ 编译器:“复制这个对象,但我不关心复制后它的内容,所以如果这能让你做一些有效的优化,那就去把你自己打倒吧”。就这样。该对象将以某种有效但未指定的状态继续存在,直到其范围结束。

isn't this information still placed on the stack?

是的,是的。另一方面:从技术上讲,C++ 标准中的任何地方都没有提到“堆栈”;堆栈只是自动作用域的一种实际实现。

If I were to expand my use of the stack again, why wouldn't this information be in danger of being overridden once the stack grew

因为NewStruct仍然是一个有效的对象,它仍然存在直到函数结束,此时堆栈上的所有对象都被销毁。

如果 NewStruct 完全在堆栈上,移动它不会比复制它更有效率。要创建一个与 NewStruct 内容相同的新对象,需要将堆栈中的所有内容复制到新对象所在的位置。

移动语义对不完全在堆栈上的对象有帮助(或者,更准确地说,没有将它们的全部内容存储在对象的固定大小部分)。例如,std::stringstd::vector(或包含它们的对象)通常会有一个缓冲区,用于保存从堆中分配的可变长度数据。移动语义可以将此缓冲区的所有权从旧对象转移到新对象,从而无需分配新缓冲区并将旧缓冲区的内容复制到其中。

我认为你的误解在于你的数据实际上是如何存储的。结构对象将在堆栈​​上,包含一个 int 和一个 std::string 对象,是的。但是,实际的字符串内容存储在自由存储中,而不是堆栈中(忽略此处不重要的优化)。

现在让我们从图片中删除 std::move 和移动语义。会发生什么?该向量将在末端创建一个新元素,该元素是从您的堆栈变量复制构造的。这意味着新对象将具有 int 值的副本和 std::string 值的副本。复制 std::string 需要在空闲存储上分配另一个内存块并将数据复制到那里。此外,任何成员如 size 都被复制为你的 int。当您的堆栈变量超出范围时,它的 int 和字符串将被销毁,这会导致字符串自行清理,不会以任何方式触及新副本。之后你剩下的是向量末尾的新对象,它有自己的数据副本。

请原谅这张糟糕的图表:

搬家如何改变这一点?移动只是一种优化,可以尽可能避免不必要的复制。复制是一种有效的移动策略,如果你能做得更好,那就不是一个好策略。使用 std::move 最终会导致向量在向量末尾创建新对象时移动堆栈对象。 int 仍将被复制,因为那里没有优化。但是,该字符串可以利用不再需要此免费存储数据这一事实。新对象可以简单地窃取指针、复制大小等,并告诉移出的对象不要清理空闲存储数据(转移所有权)。

如果你能原谅对原始糟糕图表的稍微修改版本:

我们省去了为字符串分配一个单独的块,但仅此而已。其他数据仍然全部复制。当 vector 元素被移除或 vector 被销毁时,实际的字符串数据将被 vector 元素清除。堆栈元素现在没有什么要清理的,因为移动已经“窃取”了字符串数据而不是复制它。您可以将其视为清空堆栈对象的指针,即使实现可以自由地以不同方式表示空字符串。


我所说的并不完全适用于此,因为 std::string 的主要实现足够聪明,可以避免为小字符串进行额外分配。这仅仅意味着需要复制字符串数据,因为正如您所说,它会在原始对象消失时消失。不过,任何额外的分配都可以进行移动优化。


至于你的第二个例子,你不能做一般的优化来移动原始指针;它(通常)只有 4 或 8 个字节可以复制。将其移动到向量中会复制指针值,一旦函数结束,就会导致向量中出现悬空指针。 myStr 函数中的指针将在函数结束时被销毁,不会以任何方式影响向量。