Так як конструктор копій
MyClass(const MyClass&);
та перевантаження оператора =
MyClass& operator = (const MyClass&);
мають майже однаковий код, той самий параметр і різняться лише при поверненні, чи можна мати спільну функцію для обох?
Так як конструктор копій
MyClass(const MyClass&);
та перевантаження оператора =
MyClass& operator = (const MyClass&);
мають майже однаковий код, той самий параметр і різняться лише при поверненні, чи можна мати спільну функцію для обох?
Відповіді:
Так. Є два загальних варіанти. Один із них - як правило, не рекомендується - це 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
працює та гарантується "не кидати" з усіма основними типами та типами покажчиків. Більшість розумних покажчиків також можна поміняти місцями, гарантуючи відсутність кидка.
operator=
з копію ctor насправді досить поганий, оскільки він спочатку ініціалізує всі значення за певним типом, аби просто замінити їх значеннями іншого об’єкта відразу.
assign
член, яка використовується як ctor копіювання, так і оператор присвоєння в деяких випадках (для легких класів). В інших випадках (ресурсомісткі / випадки використання, дескриптор / тіло) копіювання / обмін - це звичайно шлях.
Конструктор копіювання виконує першу ініціалізацію об'єктів, які раніше були необробленою пам'яттю. Оператор присвоєння OTOH замінює існуючі значення новими. Найчастіше це стосується звільнення старих ресурсів (наприклад, пам'яті) та виділення нових.
Якщо є подібність між ними, це те, що оператор присвоєння виконує знищення та побудову копії. Деякі розробники раніше фактично реалізовували призначення шляхом руйнування на місці з подальшим створенням копіювання розміщення. Однак це дуже погана ідея. (Що робити, якщо це оператор присвоєння базового класу, який викликався під час присвоєння похідного класу?)
Те, що сьогодні прийнято вважати канонічною ідіомою, використовує те swap
, що запропонував Чарльз:
MyClass& operator=(MyClass other)
{
swap(other);
return *this;
}
Тут використовується конструкція копіювання (примітка, яка other
копіюється) та знищення (вона знищується в кінці функції) - і вона також використовує їх у правильному порядку: конструкція (може провалитися) перед знищенням (не повинна провалитися).
Щось мене турбує:
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}. Цей крок, який є найбільш небезпечною дією, робиться останнім після того, як все інше буде врегульовано.
Так, помилка знищення є проблемою в будь-якій схемі. Дані або пошкоджені (скопійовані, коли ви не думали, що це так), або втрачені (звільнені, коли ви не думали, що це так). Втрачене краще, ніж зіпсоване. Жодні дані не кращі за погані дані.
Передача замість обміну. Це моя пропозиція в будь-якому випадку.
First, reading the word "swap" when my mind is thinking "copy" irritates
-> Як бібліотекар, ви зазвичай знаєте загальноприйняті практики (копіювання + обмін), і суть полягає в цьому my mind
. Ваш розум насправді прихований за загальнодоступним інтерфейсом. Ось у чому полягає код багаторазового використання.