Конструктор копіювання та = перевантаження оператора в C ++: чи можлива загальна функція?


87

Так як конструктор копій

MyClass(const MyClass&);

та перевантаження оператора =

MyClass& operator = (const MyClass&);

мають майже однаковий код, той самий параметр і різняться лише при поверненні, чи можна мати спільну функцію для обох?


6
"... мають майже однаковий код ..."? Хм ... Ви, мабуть, робите щось не так. Спробуйте мінімізувати необхідність використовувати для цього визначені користувачем функції і нехай компілятор виконує всю брудну роботу. Це часто означає інкапсуляцію ресурсів у їх власний об'єкт-член. Ви можете показати нам якийсь код. Можливо, ми маємо кілька хороших пропозицій щодо дизайну.
sellibitze

Відповіді:


121

Так. Є два загальних варіанти. Один із них - як правило, не рекомендується - це operator=явно викликати конструктор копіювання:

MyClass(const MyClass& other)
{
    operator=(other);
}

Однак надання товару operator=є складною справою, коли мова йде про боротьбу зі старим станом та питаннями, що виникають внаслідок самостійного призначення. Крім того, усі члени та бази спочатку отримують ініціалізацію за замовчуванням, навіть якщо їм слід призначити від other. Це може бути навіть не дійсним для всіх членів та баз, і навіть там, де воно дійсне, воно є семантично зайвим і може бути практично дорогим.

Все більш популярним рішенням є реалізація operator=за допомогою конструктора копіювання та методу підкачки.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

або навіть:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

swapФункція , як правило , просто напишіть як це просто обмінює власність нутрощів і не очиститься існуючий стан або виділяти нові ресурси.

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

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

Одне, з чим слід бути обережним, - це переконатися, що метод свопу є справжнім свопом, а не за замовчуванням, std::swapякий використовує конструктор копіювання та сам оператор присвоєння.

Зазвичай використовується членство swap. std::swapпрацює та гарантується "не кидати" з усіма основними типами та типами покажчиків. Більшість розумних покажчиків також можна поміняти місцями, гарантуючи відсутність кидка.


3
Насправді це не звичайні операції. Поки копія ctor вперше ініціалізує члени об'єкта, оператор присвоєння замінює існуючі значення. Враховуючи це, оллінг operator=з копію ctor насправді досить поганий, оскільки він спочатку ініціалізує всі значення за певним типом, аби просто замінити їх значеннями іншого об’єкта відразу.
sbi

14
Можливо, до "Я не рекомендую", додайте "і жоден фахівець з C ++ також не робить". Хтось може прийти і не зрозуміти, що ви висловлюєте не просто особисту перевагу меншини, а усталену консенсусну думку тих, хто насправді про це думав. І, добре, можливо, я помиляюся, і якийсь фахівець з C ++ це рекомендує, але особисто я все-таки простелював перчатку, щоб хтось придумав посилання на цю рекомендацію.
Steve Jessop

4
Досить справедливо, я все одно підтримав вас :-). Я вважаю, що якщо щось широко вважається найкращою практикою, тоді краще сказати так (і подивіться ще раз, якщо хтось скаже, що це насправді не найкраще). Подібним чином, якщо хтось запитав "чи можна використовувати мьютекси в C ++", я б не сказав, "одним досить поширеним варіантом є повне ігнорування RAII і написання не виняткового безпечного коду, який заблокує виробництво, але все більш популярним є написання порядний, робочий код ";-)
Стів Джессоп,

4
+1. І я думаю, що завжди потрібен аналіз. Я думаю, що розумно мати функцію- assignчлен, яка використовується як ctor копіювання, так і оператор присвоєння в деяких випадках (для легких класів). В інших випадках (ресурсомісткі / випадки використання, дескриптор / тіло) копіювання / обмін - це звичайно шлях.
Йоханнес Шауб - Litb

2
@litb: Я був здивований цим, тому я розглянув пункт 41 у винятку C ++ (на який це перетворилося), і ця конкретна рекомендація пішла, і він рекомендує скопіювати та замінити замість нього. Швидше підступно він одночасно кинув "Проблему №4: Це неефективно для призначення".
CB Bailey

13

Конструктор копіювання виконує першу ініціалізацію об'єктів, які раніше були необробленою пам'яттю. Оператор присвоєння OTOH замінює існуючі значення новими. Найчастіше це стосується звільнення старих ресурсів (наприклад, пам'яті) та виділення нових.

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

Те, що сьогодні прийнято вважати канонічною ідіомою, використовує те swap, що запропонував Чарльз:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Тут використовується конструкція копіювання (примітка, яка otherкопіюється) та знищення (вона знищується в кінці функції) - і вона також використовує їх у правильному порядку: конструкція (може провалитися) перед знищенням (не повинна провалитися).


Потрібно swapоголосити virtual?

1
@ Johanes: Віртуальні функції використовуються в поліморфних ієрархіях класів. Оператори присвоєння використовуються для типів значень. Ці два майже не змішуються.
sbi

-3

Щось мене турбує:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

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

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

Отже, я пропоную замість "своп" зробити більш природний "трансфер":

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

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

Замість {construct, move, destruct} я пропоную {construct, destruct, move}. Цей крок, який є найбільш небезпечною дією, робиться останнім після того, як все інше буде врегульовано.

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

Передача замість обміну. Це моя пропозиція в будь-якому випадку.


2
Деструктор не повинен виходити з ладу, тому винятки при руйнуванні не очікуються. І, я не розумію, що було б перевагою переміщення ходу за руйнуванням, якщо рух є найнебезпечнішою операцією? Тобто, у стандартній схемі помилка переміщення не пошкодить старий стан, тоді як ваша нова схема робить це. Так чому? Крім того, First, reading the word "swap" when my mind is thinking "copy" irritates-> Як бібліотекар, ви зазвичай знаєте загальноприйняті практики (копіювання + обмін), і суть полягає в цьому my mind. Ваш розум насправді прихований за загальнодоступним інтерфейсом. Ось у чому полягає код багаторазового використання.
Себастьян Мах,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.