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


87

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



1
Один из случаев написания собственного копиратора: когда нужно делать глубокую копию. Также обратите внимание, что как только вы создаете ctor, для вас не создается ctor по умолчанию (если вы не используете ключевое слово default).
harshvchawla

Ответы:


75

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

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

10
Он выполняет не побитовое, а пословное копирование, которое, в частности, вызывает copy-ctor для членов типа класса.
Georg Fritzsche

7
Не пишите так оператор присваивания. Это не исключение безопасно. (если новое генерирует исключение, объект остается в неопределенном состоянии с хранилищем, указывающим на освобожденную часть памяти (освобождайте память ТОЛЬКО после того, как все операции, которые могут быть выполнены, завершились успешно)). Простое решение - использовать idium copy swap.
Мартин Йорк

@sharptooth 3-я строчка снизу у вас есть, delete stored[];и я считаю, что она должна бытьdelete [] stored;
Питер Айтай

4
Я знаю, что это всего лишь пример, но вы должны указать, что лучше использовать решение std::string. Общая идея состоит в том, что только служебные классы, которые управляют ресурсами, должны перегружать «большую тройку», а все остальные классы должны просто использовать эти служебные классы, устраняя необходимость определять какой-либо из «большой тройки».
GManNickG

2
@Martin: Я хотел убедиться, что он высечен в камне. : P
GManNickG

46

Меня немного раздражает, что правило 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? Есть решения (на уровне функций попробуй / поймай ...), они просто не масштабируются.

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

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

Не знаю, как вы, но мне легче;)


Для C ++ 11 - правило пяти, которое добавляет к правилу трех конструктор Move и оператор присваивания Move.
Роберт

1
@Robb: Обратите внимание, что на самом деле, как показано в последнем примере, вы обычно должны стремиться к Правилу нуля . Только специализированные (общие) технические классы должны заботиться об обработке одного ресурса, все остальные классы должны использовать эти интеллектуальные указатели / контейнеры и не беспокоиться об этом.
Matthieu M.

@MatthieuM. Согласен :-) Я упомянул Правило пяти, так как этот ответ предшествует C ++ 11 и начинается с «Большой тройки», но следует отметить, что сейчас «Большая пятерка» актуальна. Я не хочу голосовать против этого ответа, поскольку он верен в заданном контексте.
Роберт

@Robb: Хороший замечание, я обновил ответ, упомянув Правило пяти вместо Большой тройки. Надеюсь, большинство людей уже перешли на компиляторы, поддерживающие C ++ 11 (и мне жаль тех, кто еще этого не сделал).
Matthieu M.

32

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

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


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

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

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

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

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

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

Объявите конструктор-копию с помощью =deleteat end.


Мелкая и глубокая копия

Это наиболее понятный случай и фактически единственный, упомянутый в других ответах. 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?

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

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


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

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


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

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

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

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

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

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


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

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

Примеры:

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

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

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


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

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

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


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

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

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



Оптимизация

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


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

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


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

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

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


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

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

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

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



TODOs

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

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

6

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

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


2

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


0

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


-1

Рассмотрим ниже фрагмент кода:

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();дает нежелательный вывод, потому что существует определенный пользователем конструктор копирования, созданный без кода, написанного для явного копирования данных. Таким образом, компилятор не создает то же самое.

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

Ура ... Удачного кодирования !!!

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.