赋值运算符

赋值运算符

什么是“自赋值”?

自赋值是指某人将一个对象赋值给它自己。例如:

#include "Fred.h"  // Defines class Fred

void userCode(Fred& x)
{
  x = x;           // Self-assignment
}

显然没有人会像上面那样明确地进行自赋值,但由于多个指针或引用可以指向同一个对象(别名),因此可能会在不知不觉中发生自赋值

#include "Fred.h"  // Defines class Fred

void userCode(Fred& x, Fred& y)
{
  x = y;           // Could be self-assignment if &x == &y
}

int main()
{
  Fred z;
  userCode(z, z);
  // ...
}

这仅对拷贝赋值有效。自赋值对移动赋值无效。

我为什么要担心“自赋值”?

如果你不担心自赋值,你就会让你的用户面临一些非常细微的错误,这些错误具有非常细微且通常是灾难性的症状。例如,下面的类在自赋值的情况下会导致彻底的灾难

class Wilma { };

class Fred {
public:
  Fred()                : p_(new Wilma())      { }
  Fred(const Fred& f)   : p_(new Wilma(*f.p_)) { }
 ~Fred()                { delete p_; }
  Fred& operator= (const Fred& f)
    {
      // Bad code: Doesn't handle self-assignment!
      delete p_;                // Line #1
      p_ = new Wilma(*f.p_);    // Line #2
      return *this;
    }
private:
  Wilma* p_;
};

如果有人将一个 Fred 对象赋值给它自己,第1行会删除 this->p_f.p_,因为 *thisf 是同一个对象。但第2行使用了 *f.p_,它不再是一个有效对象。这很可能会导致一场大灾难。

底线是,作为类 Fred 的作者,你有责任确保 Fred 对象的自赋值是无害的。不要假设用户永远不会对你的对象进行自赋值。如果你的对象在自赋值时崩溃,那是你的错。

附注:上面的 Fred::operator= (const Fred&) 有第二个问题:如果在评估 new Wilma(*f.p_) 时抛出异常(例如,内存不足异常Wilma 的拷贝构造函数中的异常),this->p_ 将是一个悬空指针——它将指向不再有效的内存。这可以通过在删除旧对象之前分配新对象来解决。

这仅对拷贝赋值有效。自赋值对移动赋值无效。

好的,好的,我已经明白了;我会处理自赋值。我该怎么做?

每次创建类时都应该担心自赋值。这并不意味着你需要为所有类添加额外的代码:只要你的对象能够优雅地处理自赋值,无论你是否需要添加额外的代码都无关紧要。

我们将使用上一个常见问题解答中的赋值运算符来演示这两种情况

  1. 如果自赋值可以在没有任何额外代码的情况下处理,则不要添加任何额外代码。但要添加注释,以便其他人知道你的赋值运算符优雅地处理了自赋值

    示例 1a

    Fred& Fred::operator= (const Fred& f)
    {
      // This gracefully handles self assignment
      *p_ = *f.p_;
      return *this;
    }
    

    示例 1b

    Fred& Fred::operator= (const Fred& f)
    {
      // This gracefully handles self assignment
      Wilma* tmp = new Wilma(*f.p_);   // No corruption if this line threw an exception
      delete p_;
      p_ = tmp;
      return *this;
    }
    
  2. 如果你确实需要为赋值运算符添加额外的代码,这里有一个简单有效的方法

    Fred& Fred::operator= (const Fred& f)
    {
      if (this == &f) return *this;   // Gracefully handle self assignment
      // Put the normal assignment duties here...
      return *this;
    }
    

    或等效地

    Fred& Fred::operator= (const Fred& f)
    {
      if (this != &f) {   // Gracefully handle self assignment
        // Put the normal assignment duties here...
      }
      return *this;
    }
    

顺便说一句:目标不是让自赋值变快。如果你不需要明确地测试自赋值,例如,如果你的代码在自赋值情况下也能正常工作(即使速度较慢),那么不要在赋值运算符中放置 if 测试只是为了让自赋值情况变快。原因很简单:自赋值几乎总是很少见,所以它只需要正确——它不需要高效。添加不必要的 if 语句会通过在正常情况下增加一个额外的条件分支来使罕见情况更快,惩罚多数人以造福少数人。

然而,在这种情况下,你应该在赋值运算符的顶部添加注释,表明代码的其余部分使得自赋值是良性的,这就是为什么你没有明确测试它。这样,未来的维护者就会知道要确保自赋值保持良性,否则,他们将需要添加 if 测试。

这仅对拷贝赋值有效。自赋值对移动赋值无效。

我正在创建一个派生类;我的赋值运算符是否应该调用基类的赋值运算符?

是的(如果你首先需要定义赋值运算符的话)。

如果你定义了自己的赋值运算符,编译器不会自动为你调用基类的赋值运算符。除非你的基类的赋值运算符本身是错误的,否则你应该从派生类的赋值运算符中显式调用它们(同样,假设你首先创建了它们)。

但是,如果你不创建自己的赋值运算符,编译器为你创建的那些将自动调用基类的赋值运算符。

示例

class Base {
  // ...
};

class Derived : public Base {
public:
  // ...
  Derived& operator= (const Derived& d);
  Derived& operator= (Derived&& d);
  // ...
};

Derived& Derived::operator= (const Derived& d)
{
  // Make sure self-assignment is benign
  Base::operator= (d);

  // Do the rest of your assignment operator here...
  return *this;
}

Derived& Derived::operator= (Derived&& d)
{
  // self-assignment is not allowed in move assignment
  Base::operator= (std::move(d));

  // Do the rest of your assignment operator here...
  return *this;
}