Копировать конструктор и = перегрузка оператора в С++: возможна ли общая функция?

Так как конструктор копирования

MyClass(const MyClass&);

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

MyClass& operator = (const MyClass&);

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

Ответ 1

Да. Существует два общих варианта. Один, который я не рекомендую, - это вызвать 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;
}

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

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