13.1.4. Правило три/пять

Как уже упоминалось, существуют три базовых функции, контролирующих копирование объектов класса: конструктор копий, оператор присвоения копии и деструктор. Кроме того, как будет продемонстрировано в разделе 13.6, по новому стандарту класс может также определить конструктор перемещения и оператор присваивания при перемещении.

Определять все эти функции не обязательно: вполне можно определить один или два из них, не определяя все. Эти функции можно считать модулями. Если нужен один, не обязательно определять их все.

Классы, нуждающиеся в деструкторах, нуждаются в копировании и присвоении

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

Используемый в упражнениях класс HasPtr отлично подойдет для примера (см. раздел 13.1.1). Этот класс резервирует динамическую память в конструкторе. Синтезируемый деструктор не будет удалять указатель-член. Поэтому данный класс должен определить деструктор для освобождения памяти, зарезервированной конструктором.

Хоть это и не очевидно, но согласно эмпирическому правилу класс HasPtr нуждается также в конструкторе копий и операторе присвоения копии.

Давайте посмотрим, что было бы, если бы у класса HasPtr был деструктор и синтезируемые версии конструктора копий и оператора присвоения копии:

class HasPtr {

public:

 HasPtr(const std::string &s = std::string()):

  ps(new std::string(s)), i(0) { }

 ~HasPtr() { delete ps; }

 // ошибка: HasPtr нуждается в конструкторе копий и операторе

 // присвоения копии

 // другие члены, как прежде

};

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

HasPtr f(HasPtr hp) // HasPtr передан по значению, поэтому он

                    // копируется

{

 HasPtr ret = hp; // копирует данный HasPtr

 // обработка ret

 return ret; // ret и hp удаляются

}

Когда функция f() завершает работу, объекты hp и ret удаляются и деструктор класса HasPtr выполняется для каждого из них. Этот деструктор удалит указатель-член и в объекте ret, и в объекте hp. Но эти объекты содержат одинаковое значение указателя. Код удалит тот же указатель дважды, что является серьезной ошибкой (см. раздел 12.1.2) с непредсказуемыми результатами.

Кроме того, вызывающая сторона функции f() может все еще использовать переданный ей объект:

HasPtr p("some values");

f(p); // по завершении f() память, на которую указывает p.ps,

      // освобождается

HasPtr q(p); // теперь и p, и q указывают на недопустимую память!

Память, на которую указывает указатель p (и q), больше недопустима. Она была возвращена операционной системе, когда был удален объект hp (или ret)!

Если класс нуждается в деструкторе, он почти наверняка нуждается также в операторе присвоения копии и конструкторе копий.

Классы, нуждающиеся в копировании, нуждаются также в присвоении, и наоборот

Хотя большинству классов требуется определить все функции-члены управления копированием (или ни один из них), у некоторых классов есть необходимость только в копировании или присвоении объектов, но нет никакой необходимости в деструкторе.

В качестве примера рассмотрим класс, присваивающий каждому своему объекту уникальный последовательный номер. Такому классу нужен конструктор копий для создания нового уникального последовательного номера для создаваемого объекта. Этот конструктор копировал бы все остальные переменные-члены заданного объекта. Класс нуждался бы также в собственном операторе присвоения копии, чтобы избежать присвоения объекту слева последовательного номера. Однако у этого класса не было бы никакой потребности в деструкторе.

Этот пример иллюстрирует второе эмпирическому правило: если класс нуждается в конструкторе копий, то он почти наверняка нуждается в операторе присвоения копии, и наоборот, — если класс нуждается в операторе присвоения, то он почти наверняка нуждается также в конструкторе копий. Однако нужда в конструкторе копий или операторе присвоения копии не означает потребности в деструкторе.

Упражнения раздела 13.1.4

Упражнение 13.14. Предположим, что класс numbered имеет стандартный конструктор, создающий уникальный последовательный номер для каждого объекта, который хранится в переменной-члене mysn. Класс numbered использует синтезируемые функции-члены управления копированием и имеет следующую функцию:

void f(numbered s) { cout << s.mysn << endl; }

Какой вывод создаст следующий код?

numbered a, b = a, с = b;

f(a); f(b); f(c);

Упражнение 13.15. Предположим, что у класса numbered есть конструктор копий, создающий новый последовательный номер. Изменит ли это вывод вызовов в предыдущем упражнении? Если да, то почему? Какой вывод получится?

Упражнение 13.16. Что если параметром функции f() будет const numbered&? Это изменяет вывод? Если да, то почему? Какой вывод получится?

Упражнение 13.17. Напишите версии класса numbered и функции f(), соответствующие трем предыдущим упражнениям, и проверьте правильность предсказания вывода.

Более 800 000 книг и аудиокниг! 📚

Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением

ПОЛУЧИТЬ ПОДАРОК