让我先说一下,我已经阅读了一些有关移动语义的问题.这个问题不是关于如何使用移动语义,而是询问它的目的是什么 - 如果我没有弄错,我不明白为什么需要移动语义.
我正在实施一个沉重的类,为了这个问题的目的,它看起来像这样:
class B; class A { private: std::array b; public: // ... }
在制作移动赋值运算符的时候,我意识到我可以通过将b
成员更改为std::array *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; }
这是每个人都说非常棒的强大任务:
templateT& 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]如下面的评论和答案中的讨论所述,这一点完全没有实际意义.应尽可能使用值语义来避免分配开销,因为客户端总是可以根据需要将整个事物移动到堆中.
你的例子给出了它:你的代码不是异常安全的,它使用免费存储(两次),这可能是非常重要的.要使用指针,在许多/大多数情况下,您必须在免费商店中分配内容,这比自动存储慢得多,并且不允许使用RAII.
它们还可以让您更有效地表示不可复制的资源,例如套接字.
移动语义并不是绝对必要的,因为你可以看到C++已经存在了40年 而没有它们.它们只是表示某些概念和优化的更好方法.
客户端是否已经利用指针来完成移动语义给我们的一切?如果是这样,那么移动语义的目的是什么?
你的第二个例子给出了一个很好的理由,为什么移动语义是一件好事:
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
并且有很好的理由说明它现在已经被弃用了.
用双&符号真的值得一试吗?
没错,需要一些时间来适应它.在您熟悉并熟悉它之后,您将会想知道如何在没有移动语义的情况下生活.
我非常喜欢所有的答案和评论!我同意所有这些.我只是想坚持一个没有人提到过的动机.这来自N1377:
移动语义主要是关于性能优化:能够将昂贵的对象从内存中的一个地址移动到另一个地址,同时窃取源的资源以便以最小的费用构建目标.
移动语义已在当前语言和库中存在一定程度:
在某些情况下复制构造函数elision
auto_ptr"复制"
目录::拼接
交换容器
所有这些操作都涉及将资源从一个对象(位置)转移到另一个对象(至少在概念上).缺少的是统一的语法和语义,以使通用代码能够移动任意对象(就像今天的通用代码可以复制任意对象一样).标准库中有几个地方可以大大受益于移动对象而不是复制它们的能力(将在下面深入讨论).
即在通用代码中,例如vector::erase
,需要一个统一的语法来移动值以插入被擦除的值所留下的孔.一个人不能使用,swap
因为当它value_type
是太昂贵int
.而一个不能使用拷贝赋值当那将是过于昂贵value_type
的A
(任择议定书的A
).好吧,人们可以使用拷贝分配,毕竟我们在C++ 98/03中做过,但它的价格非常昂贵.
不应该所有非原始类型的成员都是加速运动的指针
当成员类型是这样时,这将是非常昂贵的complex<double>
.不妨给它着色它.
你的字符串示例很棒.短字符串优化意味着std::string
免费存储中不存在short s:它们存在于自动存储中.
该new
/ delete
版本意味着你每强行std::string
进入自由存储.该move
版本仅将大字符串放入免费存储区,并且小字符串保留(并且可能被复制)在自动存储中.
最重要的是,您的指针版本缺少异常安全性,因为它具有非RAII资源句柄.即使您不使用异常,裸指针资源所有者也基本上强制单个退出点控制流来管理清理.最重要的是,使用裸指针所有权会导致资源泄漏和悬空指针.
所以裸指针版本在很多方面都更糟糕.
move
语义意味着您可以将复杂对象视为普通值.你move
当你不想重复的状态,copy
否则.几乎不能复制的普通类型只能公开move
(unique_ptr
),其他人可以优化它(shared_ptr
).存储在容器中的数据std::vector
现在可以包含异常类型,因为它move
知道.在标准版本的std::vector
推动std::vector
下,它的效率从低得离谱,难以使用到简单快捷.
指针将资源管理开销放入客户端,而优秀的C++ 11类为您处理该问题. move
语义使得这更容易维护,并且更不容易出错.