Операторы присваивания / FAQ C++

Добавлено 10 октября 2020 в 16:33

Что такое «самоприсваивание»?

Самоприсваивание – это когда кто-то присваивает объекту его же самого. Например,

#include "Fred.h"  // определяет класс Fred

void userCode(Fred& x)
{
  x = x;           // самоприсваивание
}

Очевидно, что никто никогда явно не выполняет самоприсваивание, подобное приведенному выше, но поскольку на один и тот же объект (псевдоним) могут указывать более одного указателя или ссылки, можно выполнять самоприсваивание, не зная об этом:

#include "Fred.h"  // определяет класс Fred

void userCode(Fred& x, Fred& y)
{
  x = y;           // может быть самоприсваиванием, если &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)
    {
      // Плохой код: не обрабатывает самоприсваивание!
      delete p_;                // строка #1
      p_ = new Wilma(*f.p_);    // строка #2
      return *this;
    }
private:
  Wilma* p_;
};

Если кто-то присваивает объект Fred ему же самому, строка #1 удаляет и this->p_, и f.p_, поскольку *this и f являются одним и тем же объектом. Но в строке #2 используется *f.p_, который больше не является допустимым объектом. Это, вероятно, вызовет серьезную катастрофу.

Суть в том, что вы, автор класса Fred, несете ответственность за обеспечение безобидности самоприсваивания объекта Fred. Не думайте, что пользователи никогда не сделают этого с вашими объектами. Это ваша вина, если ваш объект выйдет из строя, когда он получит самоприсваивание.

Кроме того, у приведенного выше Fred::operator=(const Fred&) есть вторая проблема: если при вычислении new Wilma(*f.p_) возникает исключение (например, исключение нехватки памяти или исключение в конструкторе копирования Wilma), this->p_ станет висящим указателем – он будет указывать на память, которая больше не действительна. Это может быть решено путем размещения новых объектов перед удалением старых.

Это действительно только для присваивания копирования. Самоприсваивание недействительно для присваивания перемещения.


Ладно, ладно, уже; я буду обрабатывать самоприсваивание. Как мне это сделать?

Вы должны беспокоиться о самоприсваивании каждый раз при создании класса. Это не означает, что вам нужно добавлять дополнительный код ко всем своим классам: до тех пор, пока ваши объекты элегантно обрабатывают самоприсваивание, не имеет значения, пришлось ли вам добавить дополнительный код или нет.

Мы проиллюстрируем эти два случая с помощью оператора присваивания из ответа на предыдущий вопрос:

  1. Если самоприсваивание можно обработать без дополнительного кода, не добавляйте дополнительный код. Но добавьте комментарий, чтобы другие знали, что ваш оператор присваивания элегантно обрабатывает самоприсваивание:
    • пример 1а:
      Fred& Fred::operator= (const Fred& f)
      {
        // Это элегантно обрабатывает самоприсваивание
        *p_ = *f.p_;
        return *this;
      }
    • пример 1b:
      Fred& Fred::operator= (const Fred& f)
      {
        // Это элегантно обрабатывает самоприсваивание
        Wilma* tmp = new Wilma(*f.p_);   // Не вызывает повреждений, если эта строка вызвала исключение
        delete p_;
        p_ = tmp;
        return *this;
      }
  2. Если вам нужно добавить дополнительный код к оператору присваивания, вот простой и эффективный метод:
    Fred& Fred::operator= (const Fred& f)
    {
      if (this == &f) return *this;   // Это элегантно обрабатывает самоприсваивание
      // Поместите сюда действия по обычному присваиванию...
      return *this;
    }
    Или эквивалентно:
    Fred& Fred::operator= (const Fred& f)
    {
      if (this != &f) {   // Это элегантно обрабатывает самоприсваивание
        // Поместите сюда действия по обычному присваиванию......
      }
      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)
{
  // Убедитесь, что самоприсваивание не вызывает опасений
  Base::operator= (d);
  // Здесь выполните оставшуюся часть вашего присваивания...
  return *this;
}

Derived& Derived::operator= (Derived&& d)
{
  // самоприсваивание не допускается в присваивании перемещения
  Base::operator= (std::move(d));
  // Здесь выполните оставшуюся часть вашего присваивания...
  return *this;
}

Теги

C++ / CPPFAQВысокоуровневые языки программированияОператор присваиванияПрограммированиеСамоприсваивание (программирование)Языки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.