Перегрузка конструктора копирования и оператора = в C ++: возможна ли общая функция? - PullRequest
78 голосов
/ 14 ноября 2009

С копией конструктора

MyClass(const MyClass&);

и перегрузка оператора =

MyClass& operator = (const MyClass&);

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

Ответы [ 3 ]

108 голосов
/ 14 ноября 2009

Да. Есть два общих варианта. Один - который обычно не рекомендуется - это явный вызов 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 обычно проста в написании, поскольку она просто меняет владение внутренними объектами и не требует очистки существующего состояния или выделения новых ресурсов.

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

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

Единственное, на что следует обратить внимание, это убедиться, что метод swap является истинным, а не стандартным std::swap, в котором используются конструктор копирования и сам оператор присваивания.

Обычно для каждого элемента используется swap. std::swap работает и гарантирует отсутствие броска для всех основных типов и типов указателей. Большинство умных указателей также можно поменять местами с гарантией отсутствия бросков.

12 голосов
/ 14 ноября 2009

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

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

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

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

Используется конструкция копирования (обратите внимание, что other копируется) и уничтожение (оно уничтожается в конце функции) - и оно также использует их в правильном порядке: построение (может произойти сбой) перед уничтожением ( не должен подвести).

0 голосов
/ 18 января 2010

Что-то беспокоит меня:

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, чтобы они не были освобождены дважды) ресурсов источника к нему.

Вместо {конструировать, двигать, разрушать} я предлагаю {конструировать, разрушать, перемещать}. Ход, который является самым опасным действием, является последним, предпринятым после того, как все остальное было улажено.

Да, сбой уничтожения является проблемой в любой схеме. Данные либо повреждены (скопированы, если вы об этом не думали), либо потеряны (освобождены, если вы об этом не думали). Потерянный лучше, чем испорченный. Нет данных лучше плохих.

Перевод вместо обмена. В любом случае, это мое предложение.

...