使用局部变量移动语义
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::string
或 std::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
函数中的指针将在函数结束时被销毁,不会以任何方式影响向量。
我想知道从这个例子中提取的以下代码:
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::string
或 std::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
函数中的指针将在函数结束时被销毁,不会以任何方式影响向量。