为什么要移动语义?

时间:2013-11-30 21:04:34

标签: c++ c++11 move-semantics

让我先说一下,我已经阅读了一些有关移动语义的问题。这个问题不是关于如何使用移动语义,而是询问它的目的是什么 - 如果我没有弄错,我不明白为什么需要移动语义。

背景

我正在实施一个重型课程,为了这个问题的目的,看起来像这样:

class B;

class A
{
private:
    std::array<B, 1000> b;
public:
    // ...
}

当创建移动赋值运算符时,我意识到我可以通过将b成员更改为std::array<B, 1000> *b;来显着优化过程 - 然后移动可能只是删除和指针交换。

这引出了我以下想法:现在,不应该所有非原始类型成员都指向加速运动(在[1] [2]下面更正)(有一个案例)对于不应动态分配内存的情况,但在这些情况下优化移动不是问题,因为没有办法这样做)?

这是我有以下实现的地方 - 为什么要创建一个类A,它实际上只包含一个指针b,因此当我可以简单地指向整个{{1类本身。显然,如果客户端期望移动速度明显快于复制速度,则客户端应该可以使用动态内存分配。但在这种情况下,为什么客户端不仅动态分配整个A类?

问题

客户端是否已经可以利用指针来完成移动语义给我们的一切?如果是这样,那么移动语义的目的是什么?

移动语义:

A

指针:

std::string f()
{
    std::string s("some long string");
    return s;
}

int main()
{
    // super-fast pointer swap!
    std::string a = f();
    return 0;
}

这是每个人都说的如此伟大的强大任务:

std::string *f()
{
    std::string *s = new std::string("some long string");
    return s;
}

int main()
{
    // still super-fast pointer swap!
    std::string *a = f();
    delete a;
    return 0;
}

很好 - 两个例子中的后者都可能被认为是“糟糕的风格” - 无论这意味着什么 - 但是它真的值得用双&符号来解决所有麻烦吗?如果在调用template<typename T> T& strong_assign(T *&t1, T *&t2) { delete t1; // super-fast pointer swap! t1 = t2; t2 = nullptr; return *t1; } #define rvalue_strong_assign(a, b) (auto ___##b = b, strong_assign(a, &___##b)) 之前可能抛出异常,那仍然不是一个真正的问题 - 只需做一个警卫或使用delete a

编辑[1] 我刚刚意识到,unique_ptr这样的类本身不需要使用动态内存分配并拥有高效的移动方法。这只会使我的想法失效 - 下面的问题仍然存在。

编辑[2] 正如下面的评论和答案中的讨论中提到的,这一点完全没有实际意义。应尽可能使用值语义来避免分配开销,因为客户端总是可以根据需要将整个事物移动到堆中。

4 个答案:

答案 0 :(得分:21)

我非常喜欢所有的答案和评论!我同意所有这些。我只是想坚持一个没有人提到过的动机。这来自N1377

  

移动语义主要是关于性能优化:能力   将昂贵的对象从内存中的一个地址移动到另一个地址,   同时窃取资源来构建   最低费用的目标。

     

当前语言和库中的移动语义已存在于   某种程度上:

     
      
  • 在某些情况下复制构造函数elision
  •   
  • auto_ptr“copy”
  •   
  • 列表::剪接
  •   
  • 交换容器
  •   
     

所有这些操作都涉及从一个对象传输资源   (位置)到另一个(至少在概念上)。缺少的是   统一的语法和语义,使通用代码可以任意移动   对象(就像今天的通用代码可以复制任意对象一样)。那里   是标准库中的几个地方,将大大受益   从移动物体的能力而不是复制它们(待讨论   深入下面)。

即。在通用代码(例如vector::erase)中,需要单一统一语法移动值以插入已删除值留下的漏洞。一个人无法使用swap,因为当value_typeint时,这会过于昂贵。当value_typeA {OP A)时,人们无法使用副本分配,因为这样做太昂贵了。好吧,一个可以使用拷贝分配,毕竟我们在C ++ 98/03中做了,但它的价格非常昂贵。

  

不应该所有非原始类型成员都是加速移动的指针

当成员类型为complex<double>时,这将非常昂贵。不妨给它上色.Java。

答案 1 :(得分:20)

你的例子给出了它:你的代码不是异常安全的,它使用免费商店(两次),这可能是非常重要的。要使用指针,在许多/大多数情况下,你必须在免费商店上分配东西,这比自动存储慢得多,并且不允许使用RAII。

它们还可以让您更有效地表示不可复制的资源,例如套接字。

移动语义并不是绝对必要的,因为你可以看到C ++已存在 40年 一段时间没有它们。它们只是表示某些概念和优化的更好方式。

答案 2 :(得分:15)

  

客户端是否已经可以利用指针来完成移动语义给我们的一切?如果是这样,那么移动语义的目的是什么?

你的第二个例子给出了一个很好的理由,为什么移动语义是一件好事:

std::string *f()
{
    std::string *s = new std::string("some long string");
    return s;
}

int main()
{
    // still super-fast pointer swap!
    std::string *a = f();
    delete a;
    return 0;
}

这里,客户端必须检查实现以确定谁负责删除指针。使用移动语义,这个所有权问题甚至都不会出现。

  

如果在调用delete a之前可能会抛出异常,那么这仍然不是一个真正的问题,只需要做一个警卫或使用unique_ptr

同样,如果您不使用移动语义,则会出现丑陋的所有权问题。顺便问一下,怎么样 你会在没有移动语义的情况下实现unique_ptr吗?

我知道auto_ptr并且有很好的理由说明它现在已被弃用。

  

双&符号真的值得一试吗?

没错,需要一些时间才能适应它。在您熟悉并熟悉它之后,您将会想知道如何在没有移动语义的情况下生活。

答案 3 :(得分:8)

你的字符串示例很棒。短字符串优化意味着免费存储中不存在短std::string:它们存在于自动存储中。

new / delete版本意味着您强制每个std::string进入免费商店。 move版本只将大字符串放入免费存储区,小字符串保留(并可能被复制)到自动存储中。

除此之外,您的指针版本缺少异常安全性,因为它具有非RAII资源句柄。即使您不使用异常,裸指针资源所有者也基本上强制单个退出点控制流来管理清理。最重要的是,使用裸指针所有权会导致资源泄漏和悬空指针。

所以裸指针的版本在很多方面都会变得更糟。

move语义意味着您可以将复杂对象视为普通值。当您不想要重复状态时,move,否则为copy。几乎无法复制的普通类型只能公开moveunique_ptr),其他人可以针对它进行优化(shared_ptr)。存储在容器中的数据(如std::vector)现在可以包含异常类型,因为它move可识别。 std::vector的{​​{1}}从标准版本的中风变得非常低效且难以使用,简单快捷。

指针将资源管理开销放入客户端,而优秀的C ++ 11类为您处理该问题。 std::vector语义使得这更容易维护,而且更不容易出错。