为什么需要移动语义?

18

首先声明,我已经阅读了关于移动语义的许多问题。这个问题不是关于如何使用移动语义,而是在询问它的目的 - 如果我没有错的话,我不明白为什么需要移动语义。

背景

我正在实现一个复杂的类,在这个问题中,它看起来像这样:

class B;

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

当我需要创建一个移动赋值运算符时,我意识到可以通过将b成员更改为std::array<B, 1000> *b;来显着优化过程,这样移动只需删除和指针交换即可。
这让我想到了以下问题:现在,为了加快移动速度,难道不应该所有非基本类型成员都是指针吗(下面进行了更正[1][2])(有一种情况可以说明,内存不应该是动态分配的,但在这些情况下,优化移动不是问题,因为没有办法优化)?
在这里,我有了以下认识——为什么要创建一个仅仅包含指针b的类以便于后续交换,而不是直接创建一个指向整个类的指针。如果客户端期望移动比复制要快得多,则客户端应该能够接受动态内存分配。但在这种情况下,为什么客户端不直接动态分配整个类呢?
问题是:客户端不能利用指针来实现移动语义吗?如果可以,那么移动语义的目的是什么?
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之前可能会抛出异常,那仍然不是真正的问题 - 只需设置保护或使用unique_ptr即可。

编辑[1] 我刚刚意识到对于像std::vector这样使用动态内存分配并具有高效移动方法的类来说,这是不必要的。这使我之前的想法无效了 - 下面的问题仍然存在。

编辑[2] 如下面评论和答案中讨论的那样,整个重点几乎是没有意义的。应尽可能使用值语义来避免分配开销,因为客户端始终可以将整个内容移至堆上(如果需要)。


9
为什么要有乘法?用加法也可以做同样的事情。 - Pete Becker
8
注意:移动语义的第二个原因是可以实现像 std::unique_ptr 这样的类型。(如果没有它,我们仍然会有 std::auto_ptr 的问题) - milleniumbug
2
@VF1,我们可以用加法和 while 循环来进行乘法运算,不是吗? - uk4321
1
类的一个主要目的是管理资源。它们通过封装来简化您在此处展示的指针操作。当然,您可以使用具有大型数组成员的值语义类型;然后,您可以直接使用它或将其放入管理其在自由存储器上创建和删除的类中。对于后者,移动语义很有用。 - dyp
1
这不是有点像在问:为什么要用C++,当你也可以用C完成所有的工作呢? - hyde
显示剩余5条评论
4个回答

21

我非常喜欢所有的回答和评论!我同意所有的观点。只是想再加入一个尚未提到的动机,来自N1377:

移动语义主要是关于性能优化:能够将昂贵的对象从内存中的一个地址移动到另一个地址,同时以最小的代价掠夺源对象的资源以构建目标对象。

当前语言和库中已经在一定程度上存在移动语义:

  • 在某些情况下省略复制构造函数
  • auto_ptr“复制”
  • list::splice
  • 容器的交换

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

即在通用的代码中,如vector::erase,需要一个单一统一的语法移动值以填补被删除值留下的空缺。不能使用swap,因为当value_typeint时代价太高。也不能使用复制赋值,因为当value_typeA(原帖中的A)时代价太高。当然,我们在C++98/03中确实可以使用复制赋值,但代价太高了。

所有非基本类型成员都应该是指针以加快移动速度吗?

当成员类型为complex<double>时,这将非常昂贵。 像Java一样上色也无妨。


非常好的答案,我很感激。 - VF1

20

你的例子已经泄露了漏洞:你的代码不具备异常安全性,并且使用了自由存储器(两次),这可能会很棘手。在许多/大多数情况下,要使用指针,必须在自由存储器上分配资源,这比自动存储器慢得多,并且不允许RAII。

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

虽然C++已经存在了一段时间,但移动语义并非绝对必要。它们只是表示某些概念和优化的更好方法。


7
如果没有移动语义,你需要两倍的动态内存分配。这不仅对于动态内存分配类型很重要,而且对于代表不可复制资源(如套接字)的类型也很重要。 - uk4321
什么不是异常安全的?unique_ptr难道不能缓解这个问题吗?关于双重分配,这只会出现在类本身使用动态分配存储的情况下。我想那可能更昂贵,但并不多... - VF1
10
如果您使用 unique_ptr 却没有使用移动语义,您将得到 auto_ptr。请查阅关于 auto_ptr 存在的问题的众多文章。 - uk4321
@Constantin 我不是指标准化。但是,是的,40有点太长了,已经修正。 - uk4321
2
1983年至2013年:30年 :) - user541686
显示剩余3条评论

16

客户端是否已经可以利用指针完成移动语义所能提供的所有功能?如果可以,那么移动语义的目的是什么?

你的第二个例子给出了一个非常好的原因,说明为什么移动语义是个好东西:

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,但现在它被弃用了,这其中有很好的原因。

值得为双引号符号的麻烦吗?

确实,需要一些时间来适应。当您熟悉并且舒适使用它后,您将想知道如何在没有移动语义的情况下生活。


4
所有权是原始指针不应作为函数返回值的最重要原因之一。 - Manu343726
我知道从函数中获取指针的所有权并不是很正统,但我正在寻找除了风格之外的原因(即安全性和将指针分配给对象本身的额外分配)。老实说,分配似乎并不那么昂贵,但安全性和封装性的原因似乎是有说服力的。 - VF1
@VF1 很高兴你喜欢这个答案。 - Ali

8
您的字符串示例很好。短std::string不存在于自由存储中,而是存在于自动存储中,这就是所谓的短字符串优化。
使用new/delete版本意味着您强制每个std::string进入自由存储中。使用move版本只将大字符串放入自由存储中,小字符串则留在自动存储中(并可能被复制)。
此外,指针版本缺乏异常安全性,因为它具有非RAII资源句柄。即使您不使用异常,裸指针资源所有者基本上也会强制单个退出点控制流来管理清理。此外,使用裸指针所有权会导致资源泄漏和悬空指针。
因此,裸指针版本在许多方面都更差。 move语义意味着您可以像处理常规值一样处理复杂对象。当您不希望重复状态时,可以进行move,否则进行copy。几乎不能复制的常规类型可以仅公开move(如unique_ptr),其他类型可以对其进行优化(如shared_ptr)。存储在容器中的数据,如std::vector,现在可以包括异常类型,因为它具有move感知功能。std::vectorstd::vector一下子就从难以使用的低效状态变为简单快速的状态。
指针将资源管理开销放到客户端,而好的C ++11类可为您处理此问题。 move语义使这更易于维护,并且错误更少。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接