Когда мы должны использовать конструкторы копирования?


Я знаю, что компилятор C++ создает конструктор копирования для класса. В каком случае мы должны написать пользовательский конструктор копирования? Можете ли вы привести несколько примеров?

6 72

6 ответов:

конструктор копирования, созданный компилятором, выполняет копирование по элементам. Иногда этого недостаточно. Например:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

в этом случае член-мудрое копирование stored элемент не будет дублировать буфер (будет скопирован только указатель), поэтому первым будет уничтожена копия совместного использования буфера вызовет delete[] успешно и второй будет работать в неопределенном поведении. Вам нужно глубокое копирование конструктор копирования (и оператор присваивания, как ну.)

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

я немного раздражен, что правило Rule of Five не привел.

это правило очень просто:

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

но есть более общее руководство, которому вы должны следовать, что вытекает из необходимости чтобы написать код безопасности исключений:

каждый ресурс должен управляться выделенным объектом

здесь @sharptoothкод по-прежнему (в основном) прекрасен, однако если бы он добавил второй атрибут к своему классу, это было бы не так. Рассмотрим следующий класс:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

что произойдет, если new Bar закидываешь ? Как удалить объект, на который указывает mFoo ? Есть решения (функция уровня try / catch ...), они просто не масштаб.

правильный способ справиться с ситуацией-использовать правильные классы вместо необработанных указателей.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

С той же реализацией конструктора (или фактически, используя make_unique), теперь у меня есть исключение безопасности бесплатно!!! Разве это не здорово ? И лучше всего, мне больше не нужно беспокоиться о правильном деструктор! Мне нужно написать свой собственный Copy Constructor и Assignment Operator, потому что unique_ptr не определяет эти операции... но здесь это не имеет значения ;)

и поэтому sharptoothкласс пересмотрен:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

я не знаю о вас, но я нахожу легче моего ;)

Я могу вспомнить из своей практики и думать о следующих случаях, когда приходится иметь дело с явного объявления/определение конструктора копирования. Я сгруппировал случаи в две категории

  • Правильность/Семантика - если вы не предоставляете пользовательский конструктор копирования, программы, использующие этот тип, могут не компилироваться или работать неправильно.
  • оптимизация - предоставление хорошей альтернативы сгенерированному компилятором конструктор копирования позволяет сделать программу быстрее.


Правильность/Семантика

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

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

как сделать класс не копируемым в C++03

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

как сделать класс не копируемым в C++11 или новее

объявите конструктор копирования с помощью =delete в конце.


мелкий против глубокого копирования

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

  • копирование временных файлов на диске
  • открытие отдельного сетевого соединения
  • создание отдельного рабочего потока
  • выделение отдельного фреймбуфера OpenGL
  • etc

саморегистрация объектов

рассмотрим класс, где все объекты - независимо от того, как они были построены-должны быть как-то прописаны. Некоторые примеры:

  • самый простой пример: поддержание общего количества существующих в настоящее время объектов. Регистрация объекта-это просто увеличение статического счетчика.

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

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

такая операция саморегистрации должна выполняться любым конструктором типа, и конструктор копирования не является исключением.


объекты с внутренними перекрестными ссылками

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

пример:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

разрешены только объекты, соответствующие определенным критериям скопировано

там могут быть классы, где объекты безопасны для копирования в то время как в некотором состоянии (например, default-constructed-state) и не безопасно копировать в противном случае. Если мы хотим разрешить копирование объектов safe - to - copy, то-если Программирование защищено-нам нужна проверка времени выполнения в определяемом пользователем конструкторе копирования.


не копируемые подобъекты

иногда класс, который должен быть копируемым агрегаты не копируются суб-объекты. Обычно это происходит для объектов с ненаблюдаемым состоянием (этот случай более подробно обсуждается в разделе "Оптимизация" ниже). Компилятор просто помогает распознать этот случай.


квази-копируемые подобъекты

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

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

или, как мелкое, так и глубокое копирование должно поддерживаться (ни один из них не является значением по умолчанию).

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

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


не копировать состояние, которое сильно связанный с идентификатором объекта

в редких случаях подмножество объекта наблюдаемых государство может составлять (или рассматриваться) неотделимую часть идентичности объекта и не должно передаваться другим объектам (хотя это может быть несколько спорным).

примеры:

  • UID объекта (но это также относится к случаю" саморегистрации " сверху, так как идентификатор должен быть получен в акт саморегистрации).

  • история объекта (например, стек отмены/повтора) в случае, когда новый объект не должен наследовать историю исходного объекта, а вместо этого начать с одного элемента истории"скопировано в из ".

в таких случаях конструктор копирования должен пропустить копирование соответствующих подобъектов.


обеспечение правильной подписи копии конструктор

подпись предоставленного компилятором конструктора копирования зависит от того, какие конструкторы копирования доступны для подобъектов. Если хотя бы один подобъект не имеет настоящий конструктор копирования (принимая исходный объект по постоянной ссылке), но вместо этого имеет mutating copy-constructor (принимая исходный объект по непостоянной ссылке) тогда у компилятора не будет выбора, кроме как неявно объявить, а затем определить a мутирующий конструктор копирования.

теперь, что, если" мутирующий " конструктор копирования типа подобъекта фактически не мутирует исходный объект (и был просто написан программистом, который не знает о const ключевое слово)? Если мы не можем исправить этот код, добавив отсутствующий const, тогда другой вариант-объявить наш собственный пользовательский конструктор копирования с правильной подписью и совершить грех обращаясь к const_cast.


копировать на запись (корова)

контейнер COW, который выдал прямые ссылки на его внутренние данные, должен быть глубоко скопирован во время строительства, иначе он может вести себя как дескриптор подсчета ссылок.

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



оптимизация

в следующих случаях вы можете / должны определить свой собственный конструктор копирования из соображений оптимизации:


оптимизация структуры во время копирования

рассмотрим контейнер, который поддерживает операции удаления элементов, но может сделать это, просто отметив удаленный элемент как удаленный, и повторно использовать его слот позже. Когда копия такая контейнер сделан, может иметь смысл сжать оставшиеся данные, а не сохранять "удаленные" слоты как есть.


пропустить копирование информации, не наблюдаемой на состояние

объект может содержать данные, которые не являются частью его наблюдаемого состояния. Обычно это кэшированные / записанные данные, накопленные за время существования объекта, чтобы ускорить некоторые медленные операции запроса, выполняемые объектом. Это безопасно пропустить копирование этих данных, так как это будет пересчитано когда (и если!) выполняются соответствующие операции. Копирование этих данных может быть неоправданным, так как оно может быть быстро аннулировано, если наблюдаемое состояние объекта (из которого получены кэшированные данные) изменяется путем мутирования операций (и если мы не собираемся изменять объект, почему мы создаем глубокую копию?)

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


отключить неявное копирование

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

в C++03 объявление копии конструктор также требовал его определения (конечно, если вы намеревались использовать его). Следовательно, переход на такой конструктор копирования просто выходит обсуждаемая проблема означала, что вы должны были написать тот же код, что и компилятор будет автоматически генерировать для вас.

C++11 и более новые стандарты позволяют объявлять специальные функции-члены (the конструкторы по умолчанию и копирования, оператор присваивания копий и деструктор) с явный запрос на использование по умолчанию реализация (просто закончите объявление с =default).



Тодос

этот ответ можно улучшить следующим образом:

  • добавить пример кода
  • проиллюстрируйте случай "объекты с внутренними перекрестными ссылками"
  • добавить несколько ссылок

Если у вас есть класс, который имеет динамически выделенный контент. Например, вы сохраняете название книги как символ * и устанавливаете название с новым, копировать не будет работать.

вы должны написать конструктор копирования, который делает title = new char[length+1] а то strcpy(title, titleIn). Конструктор копирования будет просто делать "мелкую" копию.

конструктор копирования вызывается, когда объект передается по значению, возвращается по значению или явно копируется. Если нет конструктора копирования, c++ создает конструктор копирования по умолчанию, который делает мелкую копию. Если объект не имеет указателей на динамически выделенную память, то будет сделано неглубокое копирование.

Это часто хорошая идея, чтобы Отключить конструктор копирования и оператор=, если классу конкретно нужно. Это может предотвратить неэффективность, такую как передача arg по значению, когда предполагается ссылка. Также компилятор генерируемых методов может быть недопустимым.