Коли ми повинні використовувати конструктори копіювання?


87

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



1
Один із випадків написання власної копії: коли вам потрібно зробити глибоку копію. Також зауважте, що як тільки ви створюєте ctor, для вас не створюється ctor за замовчуванням (якщо ви не використовуєте ключове слово за замовчуванням).
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.
Martin York

@sharptooth 3-й рядок знизу у вас є, delete stored[];і я вважаю, що це повинно бутиdelete [] stored;
Peter Ajtai

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? Є рішення (функціональний рівень 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;
};

Я не знаю про вас, але мені моє легше;)


Для C ++ 11 - правило п’яти, яке додає до правила трьох конструктор переміщення та оператор призначення переміщення.
Роберт Анджеюк,

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

@MatthieuM. Погодився :-) Я згадав Правило п'яти, оскільки ця відповідь стоїть перед C ++ 11 і починається з "Великої трійки", але слід зазначити, що зараз "Велика п'ятірка" актуальна. Я не хочу голосувати проти цієї відповіді, оскільки вона правильна в контексті запитання.
Роберт Анджеюк,

@Robb: Хороший момент, я оновив відповідь, згадавши правило п’ять замість великої трійки. Сподіваємось, більшість людей вже перейшли до компіляторів, здатних до роботи з C ++ 11 (і мені шкода тих, хто досі цього не зробив).
Matthieu M.

32

Я можу згадати свою практику і подумати про наступні випадки, коли доводиться мати справу з явним декларуванням / визначенням конструктора копіювання. Я згрупував справи у дві категорії

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


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

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

Прочитавши цей розділ, ви дізнаєтесь про кілька підводних каменів, що дозволяють компілятору самостійно генерувати конструктор копіювання. Тому, як зазначив у відповіді Сінд , завжди можна безпечно вимкнути можливість копіювання для нового класу та навмисно увімкнути його пізніше, коли це дійсно потрібно.

Як зробити клас неможливим для копіювання в C ++ 03

Оголосіть приватний конструктор копій і не надайте реалізацію для нього (так що збірка не вдається на етапі зв’язування, навіть якщо об’єкти цього типу копіюються у власній області дії класу або його друзями).

Як зробити клас неможливим для копіювання в C ++ 11 або новішої версії

Оголосіть конструктор копіювання з =deleteкінцем.


Дрібне проти глибокого копіювання

Це найкраще зрозумілий випадок і насправді єдиний, згаданий в інших відповідях. 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 об'єкта (але цей також належить до справи "самореєстрації" зверху, оскільки ідентифікатор повинен бути отриманий в акті самореєстрації).

  • Історія об'єкта (наприклад, стек "Скасувати / Повторити") у випадку, коли новий об'єкт не повинен успадковувати історію вихідного об'єкта, а натомість починати з одного елемента історії " Скопійовано о <TIME> з <OTHER_OBJECT_ID> ".

У таких випадках конструктор копіювання повинен пропустити копіювання відповідних під-об'єктів.


Застосування правильного підпису конструктора копій

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

А що, якщо "мутуючий" конструктор копій типу під-об'єкта насправді не мутує вихідний об'єкт (а був просто написаний програмістом, який не знає про constключове слово)? Якщо ми не можемо виправити цей код, додавши відсутній const, тоді інший варіант - оголосити власний конструктор копіювання, визначений користувачем, правильним підписом і здійснити гріх звернення до a const_cast.


Копіювання на запис (COW)

Контейнер COW, який видав прямі посилання на свої внутрішні дані, ПОВИНЕН бути глибоко скопійований під час побудови, інакше він може поводитися як ручка підрахунку посилань.

Хоча COW є методом оптимізації, ця логіка в конструкторі копіювання є вирішальною для правильної його реалізації. Ось чому я розмістив цю справу тут, а не в розділі "Оптимізація", куди ми підемо далі.



Оптимізація

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


Оптимізація структури під час копіювання

Розгляньте контейнер, який підтримує операції з видалення елементів, але це можна зробити, просто позначивши вилучений елемент як видалений, і пізніше переробити його слот. Коли копіюється такий контейнер, може мати сенс ущільнити збережені дані, а не зберігати "видалені" слоти як є.


Пропустити копіювання стану, що не спостерігається

Об'єкт може містити дані, які не є частиною його спостережуваного стану. Зазвичай це кешовані / запам'ятовувані дані, накопичені протягом життя об'єкта, щоб прискорити певні операції повільного запиту, що виконуються об'єктом. Копіювання цих даних безпечно пропустити, оскільки вони будуть перераховані, коли (і якщо!) Виконуються відповідні операції. Копіювання цих даних може бути невиправданим, оскільки може бути швидко недійсним, якщо спостережуваний стан об'єкта (з якого походять кешовані дані) змінюється за допомогою операцій мутації (і якщо ми не збираємося модифікувати об'єкт, чому ми створюємо глибокий скопіювати тоді?)

Ця оптимізація виправдана лише в тому випадку, якщо допоміжні дані великі порівняно з даними, що представляють спостережуваний стан.


Вимкнути неявне копіювання

C ++ дозволяє вимкнути неявне копіювання, оголосивши конструктор копіювання explicit. Тоді об'єкти цього класу не можуть бути передані у функції та / або повернені з функцій за значенням. Цей трюк можна використовувати для типу, який видається легким, але дійсно дуже дорогим для копіювання (однак, зробити його квазікопіювальним може бути кращим вибором).

У C ++ 03 декларування конструктора копіювання вимагало також його визначення (звичайно, якщо ви мали намір його використовувати). Отже, вибір такого конструктора копій просто з-за обговорюваної проблеми означав, що вам потрібно було написати той самий код, який компілятор автоматично створить для вас.

C ++ 11 та новіші стандарти дозволяють оголошувати спеціальні функції-члени (конструктори за замовчуванням та копіювання, оператор присвоєння копії та деструктор) із явним запитом на використання реалізації за замовчуванням (просто закінчіть декларацію з =default).



ЗАВДАННЯ

Цю відповідь можна покращити наступним чином:

  • Додайте більше прикладу коду
  • Проілюструйте випадок "Об'єкти з внутрішніми перехресними посиланнями"
  • Додайте кілька посилань

6

Якщо у вас є клас, який динамічно розподіляє вміст. Наприклад, ви зберігаєте заголовок книги як символ * і встановлюєте заголовок новим, копія не буде працювати.

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


2

Конструктор копіювання викликається, коли об’єкт передається за значенням, повертається за значенням або явно копіюється. Якщо конструктора копій немає, c ++ створює конструктор копій за замовчуванням, який робить поверхневу копію. Якщо об'єкт не має покажчиків на динамічно виділену пам'ять, тоді буде виконана неглибока копія.


0

Часто корисно вимкнути copy 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.