所以我对移动语义的理解是它们允许你覆盖用于临时值(rvalues)的函数,并避免可能昂贵的副本(通过将状态从未命名的临时值移动到你的命名左值)。
我的问题是为什么我们需要特殊的语义?为什么C ++ 98编译器不能忽略这些副本,因为编译器确定给定表达式是左值还是左值?举个例子:
void func(const std::string& s) {
// Do something with s
}
int main() {
func(std::string("abc") + std::string("def"));
}
即使没有C ++ 11的移动语义,编译器仍应该能够确定传递给func()
的表达式是否是rvalue,因此不需要临时对象的副本。那为什么要有这个区别呢?看起来这种移动语义的应用本质上是复制省略或其他类似编译器优化的变体。
作为另一个例子,为什么要使用如下代码呢?
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&
重载可以处理这两种情况:lvalues像往常一样,rvalues作为const引用(因为临时表达式按定义是const类)。由于编译器知道表达式何时是左值或右值,因此可以决定是否在rvalue的情况下忽略该副本。
基本上,为什么移动语义被认为是特殊的,而不仅仅是由C ++ 11之前的编译器执行的编译器优化?
答案 0 :(得分:8)
移动功能不会完全忽略临时副本。
存在相同数量的临时值,而不是通常调用复制构造函数,而是调用移动构造函数,允许移除原始构造函数而不是创建独立副本。这有时会更有效率。
C ++形式对象模型完全不受移动语义的修改。对象仍然具有明确定义的生命周期,从某个特定地址开始,并在它们被销毁时结束。他们在一生中从不“移动”。当它们“被移出”时,真正发生的事情就是从一个计划很快死亡的物体中挖出胆量,并将其有效地放置在一个新物体中。它可能看起来像是移动了,但正式地说,它们并不是真的,因为这会完全打破C ++。
搬离不是死亡。需要移动才能使对象处于“有效状态”,并且它们仍处于活动状态,并且稍后将始终调用析构函数。
Eliding副本是完全不同的东西,在某些临时对象链中,某些中间体被跳过。编译器不是必需的以便在C ++ 11和C ++ 14中删除副本,即使它可能违反“as-if”规则,它们也允许执行此操作这通常指导优化。即使复制ctor可能有副作用,高优化设置的编译器仍可能跳过一些临时工具。
相比之下,“保证复制椭圆”是一种新的C ++ 17功能,这意味着该标准要求在某些情况下进行复制椭圆。
移动语义和复制椭圆提供了两种不同的方法,可以在这些“临时链”场景中实现更高的效率。在移动语义中,所有的临时工仍然存在,但我们不是调用复制构造函数,而是调用(希望)较便宜的构造函数,移动构造函数。在复制椭圆中,我们可以一起跳过一些对象。
基本上,为什么移动语义被认为是特殊的,而不仅仅是由C ++ 11之前的编译器执行的编译器优化?
移动语义不是“编译器优化”。它们是类型系统的新部分。即使在-O0
和gcc
上使用clang
进行编译,也会发生移动语义 - 它会导致调用不同的函数,因为对象是即将死亡现在在注释类型中被“注释”。它允许“应用程序级优化”,但这与优化程序不同。
也许您可以将其视为安全网。当然,在理想的世界中,优化器总是会消除每个不必要的副本。但是,有时候,构造一个临时的很复杂,涉及动态分配,而编译器并没有全面了解它。在许多这样的情况下,您将通过移动语义来保存,这可能允许您完全避免进行动态分配。这反过来可能会导致生成的代码更容易被优化器分析。
保证副本椭圆的东西有点像,他们找到了一种方法来形成一些关于临时性的“常识”,这样更多的代码不仅可以按照预期的方式进行优化,而且必需的以编译时的方式工作,而当你认为不应该复制时不要调用复制构造函数。所以你可以,例如按工厂函数的值返回不可复制的不可移动类型。编译器发现,在进入优化器之前,在进程的早期阶段没有复制。这实际上是这一系列改进的下一次迭代。
答案 1 :(得分:3)
复制省略和移动语义并不完全相同。使用复制省略,整个对象不会被复制,它会保留在原位。随着移动,“某些东西”仍然被复制。副本并没有真正消除。但是那些“东西”是一个完整的副本必须拖拉的苍白阴影。
一个简单的例子:
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();
}
这将在这里获得完整的副本。没有任何内容被复制复制。
总之:移动语义实际上不会消除或消除副本。它只会使得结果副本“减少”。
答案 2 :(得分:2)
你对C ++中某些事情的运作有一个根本的误解:
即使没有C ++ 11的移动语义,编译器仍然应该能够确定传递给func()的表达式是一个rvalue,因此不需要临时对象的副本。
即使在C ++ 98中,该代码也不会引发任何复制。 const&
是引用而不是值。因为它是const
,它完全能够引用一个临时的。因此,使用const string&
从不的函数会获取参数的副本。
该代码将创建一个临时代码并将对该临时文件的引用传递给func
。根本没有复制。
作为另一个例子,为什么要使用如下代码呢?
没有人。函数should only take a parameter by rvalue-reference if that function will move from it。如果一个函数只是在不修改它的情况下观察该值,它们就会在const&
之前得到它,就像在C ++ 98中一样。
最重要的是:
所以我对移动语义的理解是它们允许你覆盖用于临时值(rvalues)的函数,并避免可能昂贵的副本(通过将状态从未命名的临时值移动到你的命名左值)。
你的理解是错误的。
移动不是仅关于临时值;如果是的话,我们就不会让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
存在的原因)。
答案 3 :(得分:1)
答案是移动语义不是为了消除副本而引入的。它被引入以允许/促进更便宜的复制。例如,如果类的所有数据成员都是简单整数,则复制语义与移动语义相同。在这种情况下,为此类定义move ctor和move赋值运算符是没有意义的。当课程有可以移动的东西时,移动ctor并移动任务是有意义的。
关于这个问题有很多文章。不过有些注意事项:
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)行中将使用移动ctor,并且会以更便宜的价格添加对象。请注意,对象并没有在此线路上消失,并且没有临时工具。