为什么要进行临时副本所需的移动语义



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

我的问题是为什么我们需要特殊语义?为什么C 98编译器不能将这些副本放在其中,因为这是确定给定表达式是LVALUE还是RVALUE的编译器?例如:

void func(const std::string& s) {
    // Do something with s
}
int main() {
    func(std::string("abc") + std::string("def"));
}

即使没有C 11的移动语义,编译器仍然应该能够确定传递给func()的表达式是RVALUE,因此不必要来自临时对象的副本。那么为什么完全有区别呢?似乎这种移动语义的应用本质上是复制Elision或其他类似编译器优化的变体。

作为另一个例子,为什么要打扰以下代码?

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)。由于编译器知道表达式何时是LVALUE或RVALUE,因此可以决定是否在rvalue的情况下进行副本。

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

移动函数不会完全临时副本。

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

C 形式对象模型根本不是通过移动语义修改的。物体仍然有明确的寿命,从某个特定地址开始,并在那里被摧毁时结束。他们从来没有在一生中"移动"。当它们"从"移动"时,真正发生的事情是肠道被挖出了计划很快死亡的对象,并有效地将其放在新对象中。看起来他们移动了,但是正式,他们并没有真正打破C 。

被转移到不是死亡。需要移动才能将对象留在其仍然活着的"有效状态"中,并且驱动器将始终被调用。

耗尽副本是完全不同的事情,在某些临时对象中,某些中间体被跳过。对于C 11和C 14中的ELIDE副本不是必需的编译器,即使可能违反" AS-IF"规则,它们也是允许允许这通常指导优化。那就是即使复制CTOR可能具有副作用,高优化设置处的编译器仍可能会跳过一些临时性。

相比之下,"保证复制ellision"是一个新的C 17功能,这意味着该标准需要在某些情况下进行ellision进行。

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

基本上,为什么将移动语义被认为是特殊的,而不仅仅是pre-c 11编译器可以执行的编译器优化?

移动语义不是"编译器优化"。它们是类型系统的新部分。即使您在gccclang上使用-O0编译时,移动语义也会发生 - 它会导致不同的函数被调用,因为,现在即将死去的对象即将死去的事实是在类型的类型中"注释"参考。它允许"应用程序级优化",但这与优化器不同。

也许您可以将其视为安全网。当然,在理想世界中,优化器将始终消除所有不必要的副本。但是,有时候,构建临时性是复杂的,涉及动态分配,并且编译器并没有透过这一切。在许多这样的情况下,您将通过移动语义为您节省,这可能使您完全避免进行动态分配。反过来,这可能会导致生成的代码,然后更容易进行优化器分析。

保证的副本ellision的东西有点像,他们找到了一种对临时性的某些"常识"形式化的方法,因此,更多的代码不仅可以按照优化时的期望,而且是需要在编译时按照期望的方式工作,而当您认为不应该真正有副本时,请勿致电复制构造函数。因此,您可以例如从工厂功能中返回不可恢复的,不可移动的类型。编译器在此过程中弄清楚在此过程中没有更早地发生副本。这实际上是这一系列改进的下一个迭代。

复制elision和移动语义并不完全相同。使用复制弹性,整个对象未复制,它保持在原位。采取行动,"某物"仍然被复制。该副本并未真正消除。但是,那"东西"是一个淡淡的淡淡的阴影,表明

一个简单的示例:

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()的表达式是一个rvalue,因此不需要来自临时对象的副本。

即使在C 98中,代码也不会引起任何复制。const&参考而不是值。而且由于它是const,因此它完全能够引用临时性。因此,采用const string& 从不的获得参数的副本。

该代码将创建一个临时性,并将对该代码的引用传递给func。根本没有复制。

作为另一个例子,为什么要打扰以下代码?

没人做。函数应仅通过rvalue-reference进行参数,如果该函数将从移动。如果一个函数仅观察值而不修改值,则它们会像const&一样,就像在C 98中一样。

最重要的是:

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

)。

您的理解是错误的。

移动不是关于临时值的;如果是这样,我们将不会有std::move可以让我们从lvalues移动。移动是关于将数据的所有权从一个对象传输到另一个对象。虽然临时性经常发生这种情况,但它也可能发生在lvalues中:

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对象中。它没有涉及临时工;它将内部指针的所有权转移到另一个对象。

这不是编译器可以推断出您想做的事情。您必须明确表示要在lvalue上执行这样的动作(这就是为什么std::move在那里)。

答案是移动语义的答案引入的不是消除副本。它的介绍是为了允许/促进便宜的复制。例如,如果类的所有数据成员都是简单的整数,则复制语义将相同以移动语义。在这种情况下,定义Move 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,并以较低的价格添加对象。请注意,对象没有在这条线上死亡,那里没有临时工。

最新更新