【问题标题】:Why have move semantics?为什么有移动语义?
【发布时间】:2013-12-16 19:43:24
【问题描述】:

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

背景

我正在实现一个重型类,就这个问题而言,它看起来像这样:

class B;

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

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

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

在这里我有以下认识——为什么要创建一个类A,它实际上只包含一个指针b,所以当我可以简单地创建一个指向整个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 之前可能会抛出异常,那仍然不是真正的问题 - 只需设置警卫或使用 unique_ptr

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

编辑 [2] 正如下面 cmets 和答案中的讨论所述,这一点几乎没有实际意义。应该尽可能地使用值语义来避免分配开销,因为如果需要,客户端总是可以将整个东西移动到堆中。

【问题讨论】:

  • 为什么要有乘法?你可以用加法做同样的事情。
  • 注意:移动语义的第二个原因是像std::unique_ptr 这样的类型可以实现。 (没有它,我们仍然会有std::auto_ptrsuckage)
  • @VF1 我们可以用加法和while循环来做乘法,不是吗?
  • 类的一个主要目的是管理资源。他们通过封装来简化您在此处显示的指针事物。当然,您可以使用大型数组成员创建值语义类型;那么您可以直接使用它,也可以将其放入一个类中,该类在免费商店中管理它的创建和删除。对于后者,移动语义很有用。
  • 这不是有点像在问:既然 C 也能做到这一切,为什么还要 C++?

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


【解决方案1】:

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

移动语义主要是关于性能优化:能力 将昂贵的对象从内存中的一个地址移动到另一个地址, 在盗取源头资源的同时, 以最少的费用实现目标。

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

  • 在某些情况下复制构造函数省略
  • auto_ptr "复制"
  • 列表::拼接
  • 交换容器

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

即在诸如vector::erase 之类的通用 代码中,需要一种单一统一语法移动 值来堵住被擦除值留下的漏洞。一个人不能使用swap,因为当value_typeint 时那会太贵。并且不能使用复制分配,因为当value_typeA(OP 的A)时,这将太昂贵。嗯,一个可以使用复制赋值,毕竟我们在 C++98/03 中做过,但它的成本高得离谱。

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

当成员类型为complex&lt;double&gt; 时,这将非常昂贵。还不如给它上色Java。

【讨论】:

  • 很好的答案,我很感激。
【解决方案2】:

您的示例暴露了这一点:您的代码不是异常安全的,并且它使用了免费存储(两次),这可能很重要。要使用指针,在许多/大多数情况下,您必须在空闲存储上分配东西,这比自动存储慢得多,并且不允许 RAII。

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

Move 语义并不是绝对必要的,因为您可以看到 C++ 在没有它们的情况下已经存在了 40 年 一段时间。它们只是表示某些概念的更好方法,也是一种优化。

【讨论】:

  • 您能否举一个例子,在没有动态分配的情况下,移动语义针对值类型进行了优化?即使分配发生在幕后,它仍然必须发生。
  • @VF1 没有移动语义,你有两倍的动态分配。此外,它不仅对动态分配类型很重要,而且对代表不可复制资源的类型(如套接字)也很重要。
  • 什么不是异常安全的? unique_ptr 不会缓解这个问题吗?关于双重分配,这只会在类本身使用动态分配的存储的情况下。我想这更贵,但不是很多......
  • @VF1 如果你使用没有移动语义的unique_ptr,你会得到auto_ptr。查找有关 auto_ptr 问题的大量文章。
  • 1983-2013:30 年 :)
【解决方案3】:

客户端不能利用指针来完成移动语义给我们的所有操作吗?如果是这样,那么移动语义的目的是什么?

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

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,现在不推荐使用它是有充分理由的。

双和号真的值得所有麻烦吗?

的确,习惯它需要一些时间。在您熟悉并熟悉它之后,您会想知道如果没有移动语义,您将如何生活。

【讨论】:

  • +1 所有权是最重要的原因之一,因为不应将原始指针用作函数的返回值。
  • +1。我知道从函数中获取指针的所有权并不是很正统,但我一直在寻找风格以外的原因(已经提到过 - 即安全性和指向对象本身的指针的额外分配)。不过老实说,分配似乎并没有那么昂贵,但安全和封装的原因似乎令人信服。
  • @VF1 很高兴你喜欢这个答案。
【解决方案4】:

您的字符串示例很棒。短字符串优化意味着短的std::strings 不存在于免费存储中:而是存在于自动存储中。

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

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

所以裸指针版本在很多方面都更糟糕。

move 语义意味着您可以将复杂对象视为正常值。当你不想重复状态时,你 move,否则 copy。几乎无法复制的普通类型只能暴露 move (unique_ptr),其他人可以对其进行优化 (shared_ptr)。存储在容器中的数据,例如std::vector,现在可以包含异常类型,因为它可以识别movestd::vectorstd::vector 的标准版本从效率低下且难以使用变为简单快速。

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

【讨论】:

    猜你喜欢
    • 2021-08-29
    相关资源
    最近更新 更多