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

87

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

MyClass(const MyClass&);

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

MyClass& operator = (const MyClass&);

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

MPelletier
источник
6
"... иметь примерно такой же код ..."? Хм ... Вы, должно быть, делаете что-то не так. Постарайтесь свести к минимуму необходимость использования для этого пользовательских функций и позволить компилятору делать всю грязную работу. Это часто означает инкапсуляцию ресурсов в их собственный объект-член. Вы можете показать нам код. Может быть, у нас есть хорошие предложения по дизайну.
sellibitze

Ответы:

121

Да. Есть два распространенных варианта. Один из них, который обычно не рекомендуется, - это operator=явный вызов конструктора копирования:

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

Однако предоставление товара operator=- это проблема, когда дело касается старого состояния и проблем, возникающих из-за самостоятельного назначения. Кроме того, все элементы и базы сначала инициализируются по умолчанию, даже если они должны быть назначены from 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работает, и для всех основных типов и типов указателей гарантируется отсутствие выброса. Большинство интеллектуальных указателей также можно поменять местами с гарантией отсутствия выброса.

CB Bailey
источник
3
На самом деле это не обычные операции. В то время как копирующий ctor в первый раз инициализирует элементы объекта, оператор присваивания переопределяет существующие значения. Учитывая это, объединение operator=с помощью ctor копирования на самом деле довольно плохо, потому что он сначала инициализирует все значения по умолчанию, просто чтобы сразу же переопределить их значениями другого объекта.
sbi
14
Может быть, к «Не рекомендую» добавить «и ни один эксперт по C ++». Кто-то может прийти и не понять, что вы выражаете не просто личные предпочтения меньшинства, а устоявшееся согласованное мнение тех, кто действительно думал об этом. И, ладно, возможно, я ошибаюсь, и некоторые эксперты по C ++ рекомендуют это, но лично я бы все равно бросил вызов, чтобы кто-то придумал ссылку для этой рекомендации.
Стив Джессоп,
4
Честно говоря, я уже проголосовал за вас :-). Я полагаю, что если что-то широко считается передовой практикой, то лучше так сказать (и взглянуть еще раз, если кто-то скажет, что это не совсем лучший вариант). Точно так же, если бы кто-то спросил: «Можно ли использовать мьютексы в C ++», я бы не сказал: «Один довольно распространенный вариант - полностью игнорировать RAII и писать небезопасный в отношении исключений код, который блокируется в производственной среде, но все более популярным становится написание достойный, рабочий код »;-)
Стив Джессоп
4
+1. И я думаю, что всегда нужен анализ. Я думаю, что assignв некоторых случаях (для легких классов) разумно иметь функцию- член, используемую как объектом копирования, так и оператором присваивания. В других случаях (ресурсоемкие / использующие случаи, дескриптор / тело) копирование / своп - это, конечно, путь.
Йоханнес Шауб - лит
2
@litb: Я был удивлен этим, поэтому я просмотрел пункт 41 в Exception C ++ (в который превратился этот gotw), и эта конкретная рекомендация исчезла, и он рекомендует вместо нее копировать и менять местами. Скорее всего, он украдкой отказался от «проблемы №4: это неэффективно для задания» одновременно.
CB Bailey,
13

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

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

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

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

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

SBI
источник
Должен swapбыть объявлен virtual?
1
@Johannes: Виртуальные функции используются в иерархиях полиморфных классов. Операторы присваивания используются для типов значений. Эти двое почти не смешиваются.
sbi 09
-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;
}

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

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

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

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

Мэтью
источник
2
Деструктор не должен давать сбой, поэтому исключений при уничтожении не ожидается. И я не понимаю, в чем будет преимущество перемещения движения после разрушения, если движение - самая опасная операция? Т.е. в стандартной схеме сбой перемещения не повредит старое состояние, в отличие от вашей новой схемы. Так почему? Кроме того, First, reading the word "swap" when my mind is thinking "copy" irritates-> Как писатель библиотеки, вы обычно знакомы с общепринятыми методами (копирование + замена), и суть в том, что my mind. Ваш разум фактически скрыт за публичным интерфейсом. В этом суть повторно используемого кода.
Себастьян Мах