it-swarm.com.ru

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

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

78
penguru

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

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;
}
68
sharptooth

Я немного раздражен тем, что правило 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;
};

Я не знаю как вы, но мне легче найти;)

43
Matthieu M.

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

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


/ Семантика корректности

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

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

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

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

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

Объявите конструктор копирования с =delete в конце.


Shallow vs Deep Copy

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

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

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

Рассмотрим класс, в котором все объекты - независимо от того, как они были построены - ДОЛЖНЫ быть как-то зарегистрированы. Некоторые примеры:

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

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

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

Такая операция саморегистрации должна выполняться ЛЮБЫМ конструктором типа, и конструктор копирования не является исключением.


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

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

Пример:

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?

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

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


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

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


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

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

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

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

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

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


Не копировать состояние, тесно связанное с идентичностью объекта

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

Примеры:

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

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

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


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

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

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


Копировать при записи (COW)

Контейнер COW, который дал прямые ссылки на свои внутренние данные, ДОЛЖЕН быть глубоко скопирован во время создания, иначе он может вести себя как дескриптор подсчета ссылок.

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



Оптимизация

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


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

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


Пропустить копирование ненаблюдаемого состояния

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

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


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

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

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

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



Todos

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

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

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

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

6
Peter Ajtai

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

2
josh

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

0
seand