6

所以我对移动语义的理解是,它们允许您覆盖用于临时值(右值)的函数并避免潜在的昂贵副本(通过将状态从未命名的临时移动到命名的左值)。

我的问题是为什么我们需要特殊的语义呢?为什么 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 之前的编译器可以执行的编译器优化?

4

4 回答 4

9

确切地说,移动功能不会删除临时副本。

存在相同数量的临时对象,只是通常不调用复制构造函数,而是调用移动构造函数,它允许蚕食原始对象而不是制作独立的副本。这有时可能效率更高。

C++ 形式对象模型根本没有被移动语义修改。对象仍然具有明确定义的生命周期,从某个特定地址开始,并在它们在那里被销毁时结束。他们一生中从不“移动”。当它们被“移出”时,真正发生的是从一个计划很快死亡的物体中挖出内脏,并有效地放入一个新物体中。看起来他们移动了,但从形式上看,他们并没有真正移动,因为这会完全破坏 C++。

离开不是死亡。需要移动以使对象处于它们仍然活着的“有效状态”,并且总是会在以后调用析构函数。

删除副本是完全不同的事情,在一些临时对象链中,一些中间对象被跳过。编译器不需要在 C++11 和 C++14 中删除副本,即使它可能违反通常指导优化的“as-if”规则,它们也被允许这样做。也就是说,即使复制 ctor 可能有副作用,高优化设置下的编译器仍可能会跳过一些临时变量。

相比之下,“保证复制省略”是 C++17 的一项新功能,这意味着该标准要求在某些情况下发生复制省略。

移动语义和复制省略提供了两种不同的方法来在这些“临时链”场景中实现更高的效率。在移动语义中,所有的临时对象仍然存在,但是我们没有调用复制构造函数,而是调用了一个(希望)更便宜的构造函数,即移动构造函数。在复制省略中,我们可以一起跳过一些对象。

基本上,为什么移动语义被认为是特殊的,而不仅仅是 C++11 之前的编译器可以执行的编译器优化?

移动语义不是“编译器优化”。它们是类型系统的一个新部分。即使您使用-O0on 进行gcc编译,移动语义也会发生clang- 它会导致调用不同的函数,因为对象即将死亡的事实现在已在引用类型中“注释”。它允许“应用程序级优化”,但这与优化器所做的不同。

也许您可以将其视为安全网。当然,在理想世界中,优化器总是会消除所有不必要的副本。但是,有时构造一个临时对象很复杂,涉及到动态分配,而且编译器并没有看透这一切。在许多这样的情况下,你会被移动语义所拯救,这可能会让你完全避免进行动态分配。这反过来可能会导致生成的代码更容易被优化器分析。

保证复制省略的东西有点像,他们找到了一种方法来形式化一些关于临时的“常识”,这样更多的代码不仅可以在优化时按照你期望的方式工作,而且需要按照你的方式工作期望当它被编译时,而不是在你认为不应该有副本时调用副本构造函数。因此,您可以例如从工厂函数按值返回不可复制、不可移动的类型。编译器发现在这个过程的早期没有复制发生,甚至在它到达优化器之前。这确实是这一系列改进的下一次迭代。

于 2016-12-06T03:57:42.060 回答
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();
}

这将在这里得到一个完整的复制省略。没有任何东西被复制。

总之:移动语义实际上并没有消除或消除副本。它只会使生成的副本“更少”。

于 2016-12-06T03:59:57.567 回答
2

您对 C++ 中某些事物的工作方式存在根本性的误解:

即使没有 C++11 的移动语义,编译器仍然应该能够确定传递给 func() 的表达式是右值,因此不需要从临时对象复制。

即使在 C++98 中,该代码也不会引起任何复制。Aconst&参考而不是值。因为它是const,所以它完全能够引用一个临时的。因此,采用 a 的函数const string& 永远不会获得参数的副本。

该代码将创建一个临时文件并将对该临时文件的引用传递给func. 根本不会发生复制。

再举一个例子,为什么要费心编写如下代码?

没有人会。如果该函数将从它移动,则该函数应仅通过右值引用获取参数。如果一个函数只观察值而不修改它,它们会采用const&,就像在 C++98 中一样。

最重要的是:

所以我对移动语义的理解是,它们允许您覆盖用于临时值(右值)的函数并避免潜在的昂贵副本(通过将状态从未命名的临时值移动到命名的左值)。

你的理解是错误的。

搬家不仅仅关乎临时价值;如果是这样,我们就不会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存在)。

于 2016-12-06T04:25:16.103 回答
1

答案是引入移动语义不是为了消除副本。引入它是为了允许/促进更便宜的复制。例如,如果一个类的所有数据成员都是简单的整数,复制语义将与移动语义相同。在这种情况下,为此类定义移动 ctor 和移动赋值运算符是没有意义的。当班级有可以移动的东西时,移动 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 并以更便宜的价格添加对象。请注意,对象并没有在这条线上死亡,并且那里没有临时工。

于 2016-12-06T03:58:01.320 回答