【问题标题】:Are there situations when self-assignment is useful?是否存在自我分配有用的情况?
【发布时间】:2019-10-12 10:51:32
【问题描述】:

众所周知,在实现赋值运算符时,必须防止自赋值,至少当类具有非 POD 成员时。通常是(或等价于):

Foo& operator=(const Foo& other)
{
  if (&other == this)
     return *this;
  ... // Do copy
}

没有自动插入自赋值保护的原因是什么?是否存在自我分配做一些重要且实用的事情的用例?

Foo& operator=(const Foo& other)
{
  if (&other == this)
  {
    // Do something non-trivial
  }
  else
  {
    // Do copy
  }
  return *this;
}

现在总结一下答案和讨论

看起来非平凡的自我分配永远不会真正有用。提出的唯一选择是在其中放置一个assert 以检测一些逻辑错误。但也有像a = std::min(a, b) 这样相当合理的自我分配案例,所以即使是这个选项也很可疑。

但是有两种可能的微不足道的自赋值实现:

  1. 如果&other == this 什么都不做。始终有效,但由于额外的分支可能会对性能产生负面影响。但在用户定义的赋值运算符中,测试几乎总是必须显式进行。
  2. 将每个成员复制到自身。这是默认完成的。如果成员也使用默认赋值运算符,可能会更快,因为不需要额外的分支。

我仍然不明白为什么 C++ 标准不能在用户定义的赋值运算符&other != this 中保证这一点。如果您不想分支,请使用默认运算符。如果要重新定义运算符,无论如何都需要进行一些测试...

【问题讨论】:

  • 这是一个有趣的问题。我什至会说,通常自赋值是某种逻辑错误的指标,使用断言或其他形式的严格检查是有意义的。我绝对可以想象它是无害的,但没有用。
  • 好吧,C++ 标准库已经疯狂到可以通过有效的操作重载<<>>。也许他们认为有人可能想用= 做同样疯狂的事情。
  • 条件分支可能会让“check and might do nothing”比“just do it”慢,所以默认不应该做check。
  • 我还可以想象您想知道它发生频率的情况,可能是出于性能原因。如果语言要“隐藏”自我分配,这将很难分析。
  • 你可能会发现这是一个有趣的阅读:ericniebler.com/2017/03/31/post-conditions-on-self-move

标签: c++ assignment-operator


【解决方案1】:

我应该承认我从来没有听说过这样的常识。对于非 POD 对象,更严格的方法是通过禁用复制构造函数和赋值运算符来禁止复制它们。这样你就完全没有问题了。

如果您仍然需要复制该类,但有一些数据无法安全复制到自身,您只能覆盖该数据的赋值,并且当它用作字段时,它将被自动使用上层类的赋值实现。

至于你的问题,如果你只需要跳过做任何事情,那么自动“自我分配保护”在某种程度上已经存在。如果没有明确定义赋值操作,让编译器使用自动赋值操作,内联后自赋值可能会变成无操作。

例如下面的代码:

class A {
    int a;
    double b;
};

A& foo(A& input)
{
    return (input = input);
}

编译为 (gcc 4.9, -O2):

_Z3fooR1A:
    .cfi_startproc
    movq    %rdi, %rax
    ret
    .cfi_endproc

不会复制任何东西。

【讨论】:

  • "更严格的方法是通过禁用复制构造函数和赋值运算符来禁止复制它们。" 那么移动呢?请注意,OP 并未具体说明自我分配的形式:复制或移动。虽然许多更复杂的类型无法合理复制,但很少有类型无法在逻辑上移动。
  • @NicolBolas OP 的代码使用复制,我的意思是复制。我想移动下一段也适用。
【解决方案2】:

有可能发生这种情况的算法。

  1. 您知道 lhs 和 rhs 可能是相同的,但分配比检查更简单。例如,考虑a = std::min(a,b); - 比if (a > b) a = b; 更简单,也许更容易理解 - 现在考虑更复杂的类似事情的例子。

  2. 你不知道 lhs 和 rhs 是否相同,因为它们可能是从其他地方传入的。

这些可能发生的算法并不少见。

【讨论】:

  • 问题是关于一个非平凡的自我分配。
  • 我想说可能处理的自赋值使情况(1)更加复杂,因为我们首先需要在比较 ab 时进行分支,然后可能在检查自赋值时进行分支只有一个分支。
【解决方案3】:

自赋值保护仅对那些被跳过的代码应用于自身时很危险的类型才是必需的。考虑您有一个用户提供的赋值运算符的情况,因为每个单独的对象都有某种标识符,您不想复制它。好吧,您可以在自我分配的情况下“复制”其他值。因此,插入一个不可见的自分配测试只是添加了一个毫无意义且可能代价高昂的条件分支。

因此,自我分配不是有用的;这是关于自我分配,并不总是需要保护。

此外,C++ 通常不喜欢在没有您明确要求的情况下将这样的代码添加到您的代码中。它通常是根据整个功能而不是功能的一部分来完成的。当您将要销毁的对象放入堆栈时,甚至在块末尾调用析构函数也是您所要求的。

【讨论】:

  • OP 是在询问自分配是否有用的情况,而不是自分配保护是否必要或何时应该实施。我也不得不不同意“自赋值保护仅对那些被跳过的代码应用于自身时很危险的类型才是必需的。” 即使自赋值本身是危险的,这样的检查也会非常有用。在调用代码中捕获问题完全无害。像foo[i] = foo[i] 而不是foo[i] = foo[j] 之类的拼写错误或类似的问题经常发生。
  • @VTT:帖子的第二句是“没有自动插入自赋值保护的原因是什么?”之所以存在主要问题,是因为假设如果自我分配的概念没有一些实用性,这种自动保护会存在。我的帖子反驳了这个假设,一旦反驳,这个问题就变得无关紧要了。
  • @VTT "Typos like foo[i] = foo[i] 而不是 foo[i] = foo[j]" 所提供的方法不会报告错字,因此不清楚它如何提供帮助修复它们。
猜你喜欢
  • 1970-01-01
  • 2017-09-16
  • 2015-06-02
  • 2013-08-12
  • 2012-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多