提问者:小点点

为什么移动语义学需要省略临时副本?


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

我的问题是为什么我们需要特殊的语义学?为什么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::字符串

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


共3个答案

匿名用户

确切地说,移动函数不会省略临时副本。

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

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

被移动不是死亡。需要移动才能使对象处于仍然存活的“有效状态”,并且析构函数将始终在以后调用。

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

相比之下,“保证复制逃逸”是一个新的C17特性,这意味着该标准要求在某些情况下进行复制逃逸。

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

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

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

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

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

匿名用户

复制省略和移动语义学并不完全相同。使用复制省略,整个对象不会被复制,而是停留在原地。移动后,“某样东西”仍然会被复制。副本并没有真正被消除。但是那个“某样东西”是一个完整副本必须携带的苍白阴影。

一个简单的例子:

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

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

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

匿名用户

您对C中某些内容的工作方式存在根本误解:

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

该代码不会引发任何复制,即使在C 98中也是如此。一个常量

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

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

没有人这样做。一个函数应该只接受右值引用的参数,如果该函数将从它移动。如果一个函数只观察值而不修改它,它们通过const获取它

最重要的是:

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

你的理解是错误的。

移动不仅仅是临时值;如果是,我们就不会有std::移动允许我们从左值移动。移动是将数据的所有权从一个对象转移到另一个对象。虽然这种情况经常发生在临时对象上,但左值也可能发生:

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::移动在那里)。