标准库中自我赋值 - 不安全移动赋值运算符的基本原理是什么?

Mat*_*lia 13 c++ move-semantics c++11 move-assignment-operator

关于移动分配的标准库策略是允许实现假设永远不会发生自我分配 ; 在我看来这是一个非常糟糕的主意,因为:

  • C++中的"常规"("复制")转让合同一直被认为是安全的,不能自我分配; 现在我们还有另一个C++记忆和解释的不连贯的角落案例 - 以及一个微妙危险的案例; 我想我们都同意C++中需要的不是更隐藏的陷阱;
  • 它使算法复杂化 - remove_if家庭中的任何事情都需要照顾这个角落的情况;
  • 实现这个要求真的很容易 - 你用swap实现移动它是免费的,甚至在其他情况下(你可以通过ad-hoc逻辑获得一些性能提升)它只是一个,(几乎)从不采取分支,在任何CPU¹上几乎是免费的; 此外,在大多数有趣的情况下(涉及参数或本地的移动),优化器在内联时将完全删除分支(对于"简单"移动赋值运算符几乎总是会发生这种情况).

那么,为什么这样的决定呢?


¹特别是在库代码中,实现者可以自由地利用关于"分支预期结果"的编译器特定提示(在VC++ __builtin_expect中的gcc/__assume中).

Yak*_*ont 5

std应该被丢弃的对象移动或在被重用之前被分配. 任何不完全自由的东西都不会被承诺.

有时事情是免费的.就像一个移动构造的容器是空的.请注意,某些移动辅助的情况没有这样的保证,因为某些实现可能会选择移动元素而不是缓冲区.为什么不同?一个是免费的额外保证,另一个不是.

分支机构或其他支票不是完全免费的.它占用了一个分支预测槽,即使预测它几乎是免费的.

最重要的是,a = std::move(a);是逻辑错误的证据.分配 - 来自a(内std)意味着您只会分配或丢弃a.然而,在这里,你希望它在下一行具有特定的状态.要么你知道你是自我分配,要么你不知道.如果你不这样做,你现在正在从你正在填充的对象移动,而你却不知道它.

"做一件小事以保证安全"的原则与"你不为你不使用的东西付钱"相冲突.在这种情况下,第二个赢了.

  • 性能借口完全是荒谬的; 我希望看到至少*一个*基准,这与任何相关性(我可以安全地打赌我最珍贵的财产,委员会中没有人见过这样的基准).最重要的是,当您的性能问题开始成为一个额外的分支预测器插槽时,您将停止使用STL或您无法完全控制的任何通用代码,从头开始编写适合您的问题的东西*正好*正好*您想要的,并在每次配置更改时检查关键循环的汇编输出. (3认同)

How*_*ant 5

Yakk给出了一个很好的答案(与往常一样并被赞成),但是这次我只想补充一点信息。

在过去的五年中,自我分配的政策发生了微小的变化。我们刚刚在LWG 2468中阐明了这种极端情况。实际上,我应该更加精确:两次会议之间的一个非正式小组同意解决该问题,下个月(2016年11月)很可能将其投票通过C ++ 1z工作草案。

问题的要点是修改MoveAssignable要求以阐明如果移动分配的目标和源是同一对象,则分配后对对象的值没有任何要求(除非它必须是有效状态) )。它进一步阐明,如果将此对象与std :: lib一起使用,则无论它是移动分配还是自移动分配,它仍必须满足算法的要求(例如LessThanComparable)。

所以...

T x, y;
x = std::move(y);  // The value of y is unspecified and x == the old y
x = std::move(x);  // The value of x is unspecified
Run Code Online (Sandbox Code Playgroud)

但是两者xy仍处于有效状态。没有内存泄漏。没有发生未定义的行为。

此职位的理由

它仍然是性能。但是,公认的是swap(x, x)自C ++ 98起合法,并且确实发生在野外。此外,由于C ++ 11 swap(x, x)在以下位置执行自移动分配x

T temp = std::move(x);
x = std::move(x);
x = std::move(temp);
Run Code Online (Sandbox Code Playgroud)

在C ++ 11之前,swap(x, x)是(相当昂贵的)无操作(使用复制而不是移动)。 LWG 2468阐明,在C ++ 11及之后的版本中,swap(x, x)仍然是(不是很贵)无操作(使用move而不是copy)。

细节:

T temp = std::move(x);
// temp now has the value of the original x, and x's value is unspecified
x = std::move(x);
// x's value is still unspecified
x = std::move(temp);
// x's value now has the value of temp, which is also the original x value
Run Code Online (Sandbox Code Playgroud)

要实现此无操作,x只要将其x置于有效状态而不声明或引发异常,就可以进行自我移动分配。

如果要为您的类型指定Tself-move-assignment为no-op,那就很好了。std :: lib正是针对unique_ptr

如果您想为自己的类型指定Uself-move-assignment使其处于有效但未指定的状态,那也可以。std :: lib正是针对vector。一些实现(我相信VS)会在vector无操作上进行自我分配。其他则没有(例如libc ++)。