让我先说一下,我已经阅读了一些有关移动语义的问题。这个问题不是关于如何使用移动语义,而是询问它的目的是什么 - 如果我没有弄错,我不明白为什么需要移动语义。
我正在实施一个重型课程,为了这个问题的目的,看起来像这样:
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] 正如下面的评论和答案中的讨论中提到的,这一点完全没有实际意义。应尽可能使用值语义来避免分配开销,因为客户端总是可以根据需要将整个事物移动到堆中。
答案 0 :(得分:21)
我非常喜欢所有的答案和评论!我同意所有这些。我只是想坚持一个没有人提到过的动机。这来自N1377:
移动语义主要是关于性能优化:能力 将昂贵的对象从内存中的一个地址移动到另一个地址, 同时窃取资源来构建 最低费用的目标。
当前语言和库中的移动语义已存在于 某种程度上:
- 在某些情况下复制构造函数elision
- auto_ptr“copy”
- 列表::剪接
- 交换容器
所有这些操作都涉及从一个对象传输资源 (位置)到另一个(至少在概念上)。缺少的是 统一的语法和语义,使通用代码可以任意移动 对象(就像今天的通用代码可以复制任意对象一样)。那里 是标准库中的几个地方,将大大受益 从移动物体的能力而不是复制它们(待讨论 深入下面)。
即。在通用代码(例如vector::erase
)中,需要单一统一语法到移动值以插入已删除值留下的漏洞。一个人无法使用swap
,因为当value_type
为int
时,这会过于昂贵。当value_type
为A
{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
。几乎无法复制的普通类型只能公开move
(unique_ptr
),其他人可以针对它进行优化(shared_ptr
)。存储在容器中的数据(如std::vector
)现在可以包含异常类型,因为它move
可识别。 std::vector
的{{1}}从标准版本的中风变得非常低效且难以使用,简单快捷。
指针将资源管理开销放入客户端,而优秀的C ++ 11类为您处理该问题。 std::vector
语义使得这更容易维护,而且更不容易出错。