13.1.1. Конструктор копий

Если первый параметр конструктора — ссылка на тип класса, а все дополнительные параметры имеют значения по умолчанию, то это конструктор копий:

class Foo {

public:

 Foo(); // стандартный конструктор

 Foo(const Foo&); // конструктор копий

 // ...

}

По причинам, которые будут описаны ниже, первый параметр должен иметь ссылочный тип. Он почти всегда является ссылкой на константу, хотя вполне можно определить конструктор копий, получающий ссылку на не константу. При некоторых обстоятельствах конструктор копий используется неявно. Следовательно, конструктор копий обычно не следует объявлять как explicit (см. раздел 7.5.4).

Синтезируемый конструктор копий

Если конструктор копий не определен для класса явно, компилятор синтезирует его сам. В отличие от синтезируемого стандартного конструктора (см. раздел 7.1.4), конструктор копий синтезируется, даже если определены другие конструкторы.

Как будет продемонстрировано в разделе 13.1.6, синтезируемый конструктор копий (synthesized copy constructor) некоторых классов препятствует копированию объектов этого типа. В противном случае синтезируемый конструктор копий осуществляет почленное копирование (memberwise copy) членов своего аргумента в создаваемый объект (см. раздел 7.1.5). Компилятор по очереди копирует каждую нестатическую переменную-член заданного объекта в создаваемый.

Способ копирования каждой переменной-члена определяет ее тип: для типов класса применяется конструктор копий этого класса, а члены встроенного типа копируются непосредственно. Хотя нельзя непосредственно скопировать массив (см. раздел 3.5.1), синтезируемый конструктор копий копирует члены типа массива поэлементно. Элементы типа класса копируются с использованием конструкторов копий элементов.

Например, синтезируемый конструктор копий для класса Sales_data эквивалентен следующему:

class Sales_data {

public:

 // другие члены и конструкторы как прежде

 // объявление, эквивалентное синтезируемому конструктору копий

 Sales_data(const Sales_data&);

private:

 std::string bookNo;

 int units_sold = 0;

 double revenue = 0.0;

};

// эквивалент конструктора копий, синтезированный для класса Sales_data

Sales_data::Sales_data(const Sales_data &orig):

 bookNo(orig.bookNo), // использование конструктора копий string

 units_sold(orig.units_sold), // копирует orig.units_sold

 revenue(orig.revenue)        // копирует orig.revenue

{ }                           // пустое тело

Инициализация копией

Теперь можно полностью рассмотреть различия между прямой инициализацией и инициализацией копией (см. раздел 3.2.1):

string dots(10, '.');               // прямая инициализация

string s(dots);                     // прямая инициализация

strings2 = dots;                    // инициализация копией

string null_book = "9-999-99999-9"; // инициализация копией

string nines = string(100, '9');    // инициализация копией

При прямой инициализации от компилятора требуется использовать обычный выбор функции (см. раздел 6.4) для подбора конструктора, наилучшим образом соответствующего предоставленным аргументам. Когда используется инициализация копией (copy initialization), от компилятора требуется скопировать правый операнд в создаваемый объект, осуществляя преобразования в случае необходимости (см. раздел 7.5.4).

Инициализация копией обычно использует конструктор копий. Но, как будет продемонстрировано в разделе 13.6.2, если у класса есть конструктор перемещения, то инициализация копией иногда использует конструктор перемещения вместо конструктора копий, а пока достаточно знать, что при инициализации копией требуется либо конструктор копий, либо конструктор перемещения.

Инициализация копией осуществляется не только при определении переменных с использованием оператора =, но и при:

• передаче объекта как аргумента параметру не ссылочного типа;

• возвращении объекта из функции с не ссылочным типом возвращаемого значения;

• инициализации списком в скобках элементов массива или членов агрегатного класса (см. раздел 7.5.5)

Некоторые классы используют также инициализацию копией для резервируемых объектов. Например, библиотечные контейнеры инициализируют копией свои элементы при инициализации контейнера, либо при вызове функции insert() или функции push() элемента (см. раздел 9.3.1). Элементы, созданные функцией emplace(), напротив, отличаются прямой инициализацией (см. раздел 9.3.1).

Параметры и возвращаемые значения

Во время вызова функции с параметрами не ссылочного типа осуществляется инициализация копией (см. раздел 6.2.1). Точно так же, когда у функции не ссылочный тип возвращаемого значения, возвращаемое значение используется в точке вызова для инициализации копией результата оператора вызова (см. раздел 6.3.2).

Тот факт, что конструктор копий используется для инициализации не ссылочных параметров типа класса, объясняет, почему собственный параметр конструктора копий должен быть ссылкой. Если бы этот параметр не был ссылкой, то вызов не был бы успешным — при вызове конструктора копий должен быть использован конструктор копий для копирования аргумента, но для копирования аргумента следует вызвать конструктор копий и так далее до бесконечности.

Ограничения на инициализацию копией

Как уже упоминалось, используется ли инициализация копией или прямая инициализация, если используется инициализатор, то потребуется преобразование в явный конструктор (см. раздел 7.5.4):

vector<int> v1(10);  // ok: прямая инициализация

vector<int> v2 = 10; // ошибка: конструктор, получающий размер,

                     // является явным

void f(vector<int>); // параметр f() инициализируется копией

f(10); // ошибка: нельзя использовать явный конструктор для

       // копирования аргумента

f(vector<int>(10));  // ok: непосредственно создать временный вектор

                     // из int

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

Компилятор может обойти конструктор копий

Во время инициализации копией компилятору можно (но не обязательно) пропустить конструктор копий или перемещения и создать объект непосредственно. Таким образом, код

string null_book = "9-999-99999-9"; // инициализация копией

компилятор может выполнить так:

string null_book("9-999-99999-9"); // компилятор пропускает конструктор

                                   // копий

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

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

Упражнение 13.1. Что такое конструктор копий? Когда он используется?

Упражнение 13.2. Объясните, почему следующее объявление недопустимо:

Sales_data::Sales_data(Sales_data rhs);

Упражнение 13.3. Объясните, что происходит при копировании объектов классов StrBlob и StrBlobPtr?

Упражнение 13.4. Предположим, класс Point имеет открытый конструктор копий. Укажите каждый случай использования конструктора копий в этом фрагменте кода:

Point global;

Point foo_bar(Point arg) {

 Point local = arg, *heap = new Point(global);

 *heap = local;

 Point pa[4] = { local, *heap };

 return *heap;

}

Упражнение 13.5. Напишите с учетом следующего эскиза класса конструктор копий, копирующий все переменные-члены. Конструктор должен динамически резервировать новую строку (см. раздел 12.1.2) и копировать объект, на который указывает ps, а не сам указатель ps.

class HasPtr {

public:

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

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

private:

 std::string *ps;

 int i;

};

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

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

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