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

Поскольку конструктор копирования

MyClass(const MyClass&);

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

MyClass& operator = (const MyClass&);

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

3 ответа

Решение

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

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

Конструктор копирования впервые выполняет инициализацию объектов, которые раньше были необработанной памятью. Оператор присваивания 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, чтобы они не были освобождены дважды) ресурсов источника к нему.

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

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

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

Другие вопросы по тегам