为什么需要移动语义来消除临时副本?
Why are move semantics necessary to elide temporary copies?
所以我对移动语义的理解是,它们允许您覆盖用于临时值(右值)的函数并避免可能昂贵的副本(通过将状态从未命名的临时值移动到您命名的左值)。
我的问题是为什么我们需要特殊的语义?为什么 C++98 编译器不能省略这些副本,因为是编译器决定给定表达式是左值还是右值?例如:
void func(const std::string& s) {
// Do something with s
}
int main() {
func(std::string("abc") + std::string("def"));
}
即使没有 C++11 的移动语义,编译器仍然应该能够确定传递给 func()
的表达式是右值,因此不需要从临时对象进行复制。那么为什么要有区别呢?看起来这种移动语义的应用本质上是复制省略或其他类似编译器优化的变体。
再举一个例子,为什么要像下面这样的代码呢?
void func(const std::string& s) {
// Do something with lvalue string
}
void func(std::string&& s) {
// Do something with rvalue string
}
int main() {
std::string s("abc");
// Presumably calls func(const std::string&) overload
func(s);
// Presumably calls func(std::string&&) overload
func(std::string("abc") + std::string("def"));
}
似乎 const std::string&
重载可以处理这两种情况:照常使用左值,将右值作为 const 引用(因为根据定义,临时表达式有点 const)。由于编译器知道表达式何时是左值或右值,因此它可以决定是否在右值的情况下省略副本。
基本上,为什么移动语义被认为是特殊的,而不仅仅是 C++11 之前的编译器可以执行的编译器优化?
移动函数不会完全删除临时副本。
存在相同数量的临时对象,只是调用了移动构造函数而不是通常调用复制构造函数,它允许蚕食原始而不是独立复制。这有时可能会更有效。
C++ 形式对象模型根本没有被移动语义修改。对象仍然有一个明确定义的生命周期,从某个特定地址开始,到它们在那里被销毁时结束。他们一生中从未"move"。当它们 "moved from" 时,真正发生的是内脏从一个计划很快死亡的对象中挖出,并有效地放置在一个新对象中。看起来他们移动了,但正式地说,他们并没有移动,因为那样会完全破坏 C++。
搬离不是死亡。需要移动才能将对象留在 "valid state" 中,它们仍然存在,并且析构函数将始终在以后调用。
删除副本是完全不同的事情,在一些临时对象链中,一些中间对象被跳过。编译器 不需要 删除 C++11 和 C++14 中的副本,它们 允许 这样做,即使它可能违反"as-if" 通常指导优化的规则。也就是说,即使复制构造函数可能有副作用,处于高优化设置的编译器仍可能会跳过一些临时文件。
相比之下,"guaranteed copy ellision" 是一个新的 C++17 功能,这意味着该标准要求在某些情况下进行复制省略。
移动语义和复制省略提供了两种不同的方法来提高这些 "chain of temporaries" 场景中的效率。在移动语义中,所有的临时对象仍然存在,但我们不是调用复制构造函数,而是调用一个(希望)更便宜的构造函数,即移动构造函数。在复制省略中,我们可以一起跳过一些对象。
Basically, why are move semantics considered special and not just a compiler optimization that could have been performed by pre-C++11 compilers?
移动语义不是 "compiler optimization"。它们是类型系统的新部分。即使在 gcc
和 clang
上使用 -O0
进行编译时也会发生移动语义——它会导致调用 不同的函数 ,因为,事实上一个即将消亡的对象现在是 "annotated" 的引用类型。它允许 "application level optimizations" 但这与优化器所做的不同。
也许你可以把它想象成一个安全网。当然,在理想世界中,优化器总是会消除所有不必要的副本。但是,有时构造一个临时对象很复杂,涉及动态分配,而且编译器并不能看透它。在许多这样的情况下,移动语义会拯救你,这可能会让你完全避免进行动态分配。这反过来可能会导致生成的代码更容易被优化器分析。
保证复制省略的事情有点像,他们找到了一种方法来形式化其中一些关于临时变量的 "common sense",这样更多的代码不仅可以在优化时按照您期望的方式工作,而且要求 在编译时以您期望的方式工作,并且在您认为不应该真正存在副本时不调用复制构造函数。所以你可以,例如return 不可复制、不可移动的类型(按工厂函数的值)。编译器发现在过程中更早的时候没有发生复制,甚至在它到达优化器之前。这确实是这一系列改进的下一次迭代。
答案是引入移动语义不是为了消除副本。它被引入 allow/promote 更便宜的复制。例如,如果 class 的所有数据成员都是简单整数,则复制语义与移动语义相同。在这种情况下,为此 class 定义移动构造函数和移动赋值运算符没有意义。当 class 有可以移动的东西时,移动构造函数和移动赋值才有意义。
关于这个主题的文章很多。不过一些注意事项:
- 一旦参数被指定为
T&&
,大家都很清楚
可以窃取其内容。点。简单明了。在 C++03 中
没有明确的语法或任何其他既定约定来传达
这个想法。事实上,有很多其他方式可以表达同样的事情。
但是委员会选择了这种方式。
- 移动语义不仅对
rvalue
引用有用。可以用
任何你想表明你想通过的地方
您对该函数的对象,该函数可能会获取其内容。
您可能有这个代码:
void Func(std::vector<MyComplexType> &v)
{
MyComplexType x;
x.Set1(); // Expensive function that allocates storage
// and computes something.
......... // Ton of other code with if statements and loops
// that builds the object x.
v.push_back(std::move(x)); // (1)
x.Set2(); // Code continues to use x. This is ok.
}
请注意,在第 (1) 行中,将使用 move ctor 并以更便宜的价格添加对象。请注意,对象并没有在这一行死亡,那里没有临时对象。
复制省略和移动语义不完全相同。使用复制省略,不会复制整个对象,它会保留在原地。移动后,"something" 仍会被复制。副本并没有真正被淘汰。但是,"something" 与成熟的副本相比显得苍白无力。
一个简单的例子:
class Bar {
std::vector<int> foo;
public:
Bar(const std::vector<int> &bar) : foo(bar)
{
}
};
std::vector<int> foo();
int main()
{
Bar bar=foo();
}
祝你好运,在这里让你的编译器消除副本。
现在,添加这个构造函数:
Bar(std::vector<int> &&bar) : foo(std::move(bar))
{
}
现在,main()
中的对象是使用移动操作构建的。完全复制实际上并没有被消除,只是移动操作只是一些线噪音。
另一方面:
Bar foo();
int main()
{
Bar bar=foo();
}
这将在此处进行完整的复制省略。没有任何内容被复制。
结论:移动语义实际上并没有省略或消除副本。它只是制作结果副本 "less".
您对 C++ 中某些事物的工作原理存在根本性的误解:
Even without C++11's move semantics, the compiler should still be able to determine that the expression passed to func() is an rvalue, and thus the copy from a temporary object is unnecessary.
即使在 C++98 中,该代码也不会引发 任何 复制。 const&
是 引用 而不是值。因为它是 const
,所以它完全能够引用一个临时文件。因此,采用 const string&
never 的函数获取参数的副本。
该代码将创建一个临时文件并将对该临时文件的引用传递给 func
。根本没有复制发生。
As another example, why bother having code like the following?
没有人这样做。一个函数 should only take a parameter by rvalue-reference if that function will move from it。如果一个函数只是观察值而不修改它,他们会通过 const&
获取它,就像在 C++98 中一样。
最重要的是:
So my understanding of move semantics is that they allow you to override functions for use with temporary values (rvalues) and avoid potentially expensive copies (by moving the state from an unnamed temporary into your named lvalue).
你的理解有误。
移动不仅仅是临时值;如果是,我们就不会有 std::move
允许我们从左值移动。移动是将数据的所有权从一个对象转移到另一个对象。虽然这种情况经常发生在临时对象上,但它也可能发生在左值上:
std::unique_ptr<T> p = ...
std::unique_ptr<T> other_p = std::move(p);
assert(p == nullptr); //Will always be true.
此代码创建一个 unique_ptr,然后将该指针的内容移动到另一个 unique_ptr
对象中。它不是在处理临时工;它正在将内部指针的所有权转移到另一个对象。
这不是编译器可以推断出您想做的事情。您必须明确表示要对左值执行这样的移动(这就是 std::move
存在的原因)。
所以我对移动语义的理解是,它们允许您覆盖用于临时值(右值)的函数并避免可能昂贵的副本(通过将状态从未命名的临时值移动到您命名的左值)。
我的问题是为什么我们需要特殊的语义?为什么 C++98 编译器不能省略这些副本,因为是编译器决定给定表达式是左值还是右值?例如:
void func(const std::string& s) {
// Do something with s
}
int main() {
func(std::string("abc") + std::string("def"));
}
即使没有 C++11 的移动语义,编译器仍然应该能够确定传递给 func()
的表达式是右值,因此不需要从临时对象进行复制。那么为什么要有区别呢?看起来这种移动语义的应用本质上是复制省略或其他类似编译器优化的变体。
再举一个例子,为什么要像下面这样的代码呢?
void func(const std::string& s) {
// Do something with lvalue string
}
void func(std::string&& s) {
// Do something with rvalue string
}
int main() {
std::string s("abc");
// Presumably calls func(const std::string&) overload
func(s);
// Presumably calls func(std::string&&) overload
func(std::string("abc") + std::string("def"));
}
似乎 const std::string&
重载可以处理这两种情况:照常使用左值,将右值作为 const 引用(因为根据定义,临时表达式有点 const)。由于编译器知道表达式何时是左值或右值,因此它可以决定是否在右值的情况下省略副本。
基本上,为什么移动语义被认为是特殊的,而不仅仅是 C++11 之前的编译器可以执行的编译器优化?
移动函数不会完全删除临时副本。
存在相同数量的临时对象,只是调用了移动构造函数而不是通常调用复制构造函数,它允许蚕食原始而不是独立复制。这有时可能会更有效。
C++ 形式对象模型根本没有被移动语义修改。对象仍然有一个明确定义的生命周期,从某个特定地址开始,到它们在那里被销毁时结束。他们一生中从未"move"。当它们 "moved from" 时,真正发生的是内脏从一个计划很快死亡的对象中挖出,并有效地放置在一个新对象中。看起来他们移动了,但正式地说,他们并没有移动,因为那样会完全破坏 C++。
搬离不是死亡。需要移动才能将对象留在 "valid state" 中,它们仍然存在,并且析构函数将始终在以后调用。
删除副本是完全不同的事情,在一些临时对象链中,一些中间对象被跳过。编译器 不需要 删除 C++11 和 C++14 中的副本,它们 允许 这样做,即使它可能违反"as-if" 通常指导优化的规则。也就是说,即使复制构造函数可能有副作用,处于高优化设置的编译器仍可能会跳过一些临时文件。
相比之下,"guaranteed copy ellision" 是一个新的 C++17 功能,这意味着该标准要求在某些情况下进行复制省略。
移动语义和复制省略提供了两种不同的方法来提高这些 "chain of temporaries" 场景中的效率。在移动语义中,所有的临时对象仍然存在,但我们不是调用复制构造函数,而是调用一个(希望)更便宜的构造函数,即移动构造函数。在复制省略中,我们可以一起跳过一些对象。
Basically, why are move semantics considered special and not just a compiler optimization that could have been performed by pre-C++11 compilers?
移动语义不是 "compiler optimization"。它们是类型系统的新部分。即使在 gcc
和 clang
上使用 -O0
进行编译时也会发生移动语义——它会导致调用 不同的函数 ,因为,事实上一个即将消亡的对象现在是 "annotated" 的引用类型。它允许 "application level optimizations" 但这与优化器所做的不同。
也许你可以把它想象成一个安全网。当然,在理想世界中,优化器总是会消除所有不必要的副本。但是,有时构造一个临时对象很复杂,涉及动态分配,而且编译器并不能看透它。在许多这样的情况下,移动语义会拯救你,这可能会让你完全避免进行动态分配。这反过来可能会导致生成的代码更容易被优化器分析。
保证复制省略的事情有点像,他们找到了一种方法来形式化其中一些关于临时变量的 "common sense",这样更多的代码不仅可以在优化时按照您期望的方式工作,而且要求 在编译时以您期望的方式工作,并且在您认为不应该真正存在副本时不调用复制构造函数。所以你可以,例如return 不可复制、不可移动的类型(按工厂函数的值)。编译器发现在过程中更早的时候没有发生复制,甚至在它到达优化器之前。这确实是这一系列改进的下一次迭代。
答案是引入移动语义不是为了消除副本。它被引入 allow/promote 更便宜的复制。例如,如果 class 的所有数据成员都是简单整数,则复制语义与移动语义相同。在这种情况下,为此 class 定义移动构造函数和移动赋值运算符没有意义。当 class 有可以移动的东西时,移动构造函数和移动赋值才有意义。
关于这个主题的文章很多。不过一些注意事项:
- 一旦参数被指定为
T&&
,大家都很清楚 可以窃取其内容。点。简单明了。在 C++03 中 没有明确的语法或任何其他既定约定来传达 这个想法。事实上,有很多其他方式可以表达同样的事情。 但是委员会选择了这种方式。 - 移动语义不仅对
rvalue
引用有用。可以用 任何你想表明你想通过的地方 您对该函数的对象,该函数可能会获取其内容。
您可能有这个代码:
void Func(std::vector<MyComplexType> &v)
{
MyComplexType x;
x.Set1(); // Expensive function that allocates storage
// and computes something.
......... // Ton of other code with if statements and loops
// that builds the object x.
v.push_back(std::move(x)); // (1)
x.Set2(); // Code continues to use x. This is ok.
}
请注意,在第 (1) 行中,将使用 move ctor 并以更便宜的价格添加对象。请注意,对象并没有在这一行死亡,那里没有临时对象。
复制省略和移动语义不完全相同。使用复制省略,不会复制整个对象,它会保留在原地。移动后,"something" 仍会被复制。副本并没有真正被淘汰。但是,"something" 与成熟的副本相比显得苍白无力。
一个简单的例子:
class Bar {
std::vector<int> foo;
public:
Bar(const std::vector<int> &bar) : foo(bar)
{
}
};
std::vector<int> foo();
int main()
{
Bar bar=foo();
}
祝你好运,在这里让你的编译器消除副本。
现在,添加这个构造函数:
Bar(std::vector<int> &&bar) : foo(std::move(bar))
{
}
现在,main()
中的对象是使用移动操作构建的。完全复制实际上并没有被消除,只是移动操作只是一些线噪音。
另一方面:
Bar foo();
int main()
{
Bar bar=foo();
}
这将在此处进行完整的复制省略。没有任何内容被复制。
结论:移动语义实际上并没有省略或消除副本。它只是制作结果副本 "less".
您对 C++ 中某些事物的工作原理存在根本性的误解:
Even without C++11's move semantics, the compiler should still be able to determine that the expression passed to func() is an rvalue, and thus the copy from a temporary object is unnecessary.
即使在 C++98 中,该代码也不会引发 任何 复制。 const&
是 引用 而不是值。因为它是 const
,所以它完全能够引用一个临时文件。因此,采用 const string&
never 的函数获取参数的副本。
该代码将创建一个临时文件并将对该临时文件的引用传递给 func
。根本没有复制发生。
As another example, why bother having code like the following?
没有人这样做。一个函数 should only take a parameter by rvalue-reference if that function will move from it。如果一个函数只是观察值而不修改它,他们会通过 const&
获取它,就像在 C++98 中一样。
最重要的是:
So my understanding of move semantics is that they allow you to override functions for use with temporary values (rvalues) and avoid potentially expensive copies (by moving the state from an unnamed temporary into your named lvalue).
你的理解有误。
移动不仅仅是临时值;如果是,我们就不会有 std::move
允许我们从左值移动。移动是将数据的所有权从一个对象转移到另一个对象。虽然这种情况经常发生在临时对象上,但它也可能发生在左值上:
std::unique_ptr<T> p = ...
std::unique_ptr<T> other_p = std::move(p);
assert(p == nullptr); //Will always be true.
此代码创建一个 unique_ptr,然后将该指针的内容移动到另一个 unique_ptr
对象中。它不是在处理临时工;它正在将内部指针的所有权转移到另一个对象。
这不是编译器可以推断出您想做的事情。您必须明确表示要对左值执行这样的移动(这就是 std::move
存在的原因)。