Статья
Версия для печати
Обсудить на форуме
Классы: копирование и присваивание.

Часть 4.

Этой частью мы завершим начатое в статьях "Элементы класса, о которых всегда необходимо помнить" и "Классы: копирование и присваивание. Часть 1, Часть 2 и Часть3" подробное рассмотрение проблемы копирования и присваивания в классах.

В этой части мы рассмотрим как можно заблокировать копирование и присваивание, как можно реализовать копирование через присваивание, и обсудим проблемы копирования в производных (дочерних, а может сыновних?) классах.

Блокирование копирования и присваивания

Издержки на размещение в памяти копирование и воспроизведение простых типов данных в принципе невелики. Чем сложнее тип данных, тем больше будут "расходы" на копирование объектов этого типа. Сложные типы могут требовать нетривиального управления памятью, они могут выполнять сложные графические преобразования, могут состоять из сложных структур данных, например, связанных списков, бинарных деревьев и мультимножеств; могут отвечать за инициализацию внешних устройств или быть элементами разветвленной иерархии. Как и в большинстве вопросов конкретной реализации, именно вам придется решать, в каких обстоятельствах целесообразно заблокировать копирование.

Мы же посмотрим только как это сделать. Чтобы запретить компилятору автоматически создавать конструктор копий и операцию присваивания, надо объявить их в классе, но не писать определение (метод или функцию). При этом объявление должно располагаться в закрытой (private) секции класса.
Код:
class POINT
{
  public:
         // открытые члены
  protected:
         // защищенные члены
  private:
         // закрытые члены
   POINT(const POINT&);
   POINT& operator=(const POINT&);
         // блокировка копирования
};

Для произвольного класса POINT объявление без определения (тело функции отсутствует, его просто не надо писать) блокирует копирование, заставляя компилятор генерировать сообщение об ошибке при вызове одной из этих функций. Этот прием запрещает копирование отдельных объектов, не лишая, однако, вас возможности использовать псевдонимы (псевдонимом (если вы забыли) может быть, например, имя аргумента, через который передается ссылка на объект).

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

Реализация копирования через присваивание

В предыдущих разделах мы уже отмечали сходство в назначении и реализации конструктора копий и операции присваивания. Единственное реальное отличие состоит в том, что операция присваивания выполняет проверку на присваивание самой себе, а конструктор копий - нет, так как он вызывается для создания новых объектов, когда ни о каком присваивании самому себе не может быть и речи.

Поскольку определения этих функций почти идентичны, то их сходство можно использовать для реализации одной функции через другую. Это не только упрощает и ускоряет их написание, но и облегчает синхронизацию изменений в них.

Часто встречается такой вариант реализации:
Код:
class POINT
{
  public:
      // ... конструктор, деструктор и другие члены класса
      // конструктор копий, реализованный через (то есть
      // неявно вызывающий) операцию присваивания
    POINT(const POINT& rhs) { *this = rhs; }
      // Операция присваивания
    POINT& operator=(const POINT& rhs)
    {     
      if(this == &rhs)  return *this;
        // выполняет присваивание, развернутое копирование
        // или еще что-нибудь
      return *this;
    }
   private:
     // данные-члены
};

В строке { *this = rhs; } неявно вызывается операция присваивания (смотрим в предыдущую статью). Такая форма вызова довольно туманна и в принципе может стать причиной ошибок. Ранее все время подчеркивалось, что перегрузка операций, как правило, происходит неявно, здесь же это правило бесцеремонно нарушено. Это сделано для ясности, а также потому, что разработчику класса так или иначе следует об этом знать.

Давайте посмотрим, как более удобно реализовать копирование через присваивание.
Код:
// Конструктор копий вызывает операцию присваивания явно, 
// точно так же, как и любую другую функцию
POINT(const POINT& rhs) { operator=(rhs); }
// Операция присваивания
POINT& operator=(const POINT& rhs)
{     
  if(this == &rhs) return *this;
  // код присваивания/копирования ...
  return *this;
}

Здесь операция присваивания вызывается уже явно, подобно любой другой функции. В развернутой форме этот вызов можно представить как this->operator=(rhs);

Предварять вызов функции указателем this в принципе излишне, хотя и вполне корректно. Реализация копирования через присваивание позволяет свести разработку и отладку двух этих функций к одной - операции присваивания, независимо от дальнейшего развития и изменения вашего класса.

Все это совсем не значит, что копирование надо всегда реализовывать через присваивание, это просто один из приемов, облегчающих жизнь. Но, надо отметить, что при подобной реализации копирования происходит дополнительный вызов функции. Маловероятно, чтобы это могло существенно повлиять на производительность ваших программ, однако помнить об этом не мешает.

Копирование и присваивание в дочерних классах

Как известно, конструктор копий не наследуется. Тема наследования не входит в рамки данного материала, но если в двух словах, то наследование - это алгоритм, в соответствии с которым компилятор заимствует элементы функциональности существующих классов для создания новых.

При выборе подходящего метода копирования не имеет значения, является ли ваш класс самостоятельным или же частью разветвленной иерархической структуры. Чаще всего для производных классов используется копирование того же типа, что и для их родителей. Однако никто не запрещает вам при необходимости копирование заблокировать.

Предмет нашего обсуждения - копирование объектов производных классов. Если для родительского класса уже определены копирование и присваивание, то весь фокус состоит в том, чтобы извлечь из них максимальную пользу. Прочитав предыдущий раздел "Реализация копирования через присваивание", вы легко обеспечите копирование унаследованной части объекта. Надо просто использовать его операцию присваивания и дописать код для своей части класса. Рассмотрим следующий класс B, производный от класса А:
Код:
class В : public A // Читается так: класс В открыто выводится из А
{
  public:
     В();  //Конструктор
     ~В(); // Деструктор
     B(const B&);  // Конструктор копий
     B& operator=(const B&); // Присваивание

  private:
     // etc
};

С классом A все в порядке; он полностью определен, отлажен и работоспособен. B - это класс, который определен как потомок A. Предположим, что конструктор копий для B реализован средствами присваивания класса B, тогда:
Код:
// Конструктор копий
В::В(const B& rhs) { operator=(rhs); }
        // пусть эту работу делает операция присваивания
// Операция присваивания
B& В::operator=(const B& rhs)
{
  if(this == &rhs) return *this;
  A::operator=(rhs);    // копирование наследства, т.е. класса А
       … // а тут будет присваивание членов, свойственных только В
  return *this;
}

Итак, мы сделали единственный явный вызов операции присваивания класса A, и дело сделано. Скопировано все, что унаследовано. Если же вы не хотите реализовывать копирование через присваивание, то вам придется в конструкторе копий создать также базовый класс. Операция присваивания при этом не изменится, но код конструктора копий станет таким:
Код:
// Список инициализации в первой строке используется для вызова
// конструктора копий для унаследованной из А части В, а оставшаяся
// часть конструктора занимается копированием данных-членов
В::В(const B&) : A(rhs)
{
  // Здесь вместо вызова operator=
  // Присваиваются значения всем членам В
}

Единственное примечательное изменение состоит в том, что часть A класса B также должна дублироваться.

Вот, собственно, и все, чем я хотел с вами поделиться.
Если у вас возникает потребность подробного рассмотрения какого-либо вопроса по С++, пишите нам, мы попытаемся вам помочь.

При написании статьи использованы материалы из книг
P.Kimmel
Using Borland C++ 5
Special Edition
перевод  BHV - С.Петербург 1997

C++. Бархатный путь
Марченко А.Л.
Центр Информационных Технологий
www.citmgu.ru

Thinking in C++, 2nd ed. Volume 1
©2000 by Bruce Eckel

Сергей Малышев (aka Михалыч).
Версия для печати
Обсудить на форуме