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

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

Ответ 1

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

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[] успешно, а второй будет запущен в поведение undefined. Вам нужен глубокий копировальный экземпляр (и оператор присваивания).

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

Ответ 2

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

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

Правило пяти:
Всякий раз, когда вы пишете либо один из Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor или Move Assignment Operator, вам, вероятно, нужно написать остальные четыре.

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

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

Здесь @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 revisited:

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

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

Ответ 3

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

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


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

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

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

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

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

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

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


Неверное копирование

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

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

Саморегистрационные объекты

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

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

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

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

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


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

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

Пример:

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?

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

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


Не скопируемые под-объекты

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


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

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

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

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

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

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


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

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

Примеры:

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

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

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


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

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

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


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

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

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



Оптимизация

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


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

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


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

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

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


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

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

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

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



Todos

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

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

Ответ 4

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

Давайте рассмотрим ниже фрагмент кода:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

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

Просто подумал поделиться этим знанием со всеми вами, хотя большинство из вас уже знают это.

Ура... Счастливого кодирования !!!