Что это за идиома и когда ее следует использовать? Какие проблемы это решает? Меняется ли идиома при использовании C ++ 11?
Хотя это упоминалось во многих местах, у нас не было ни единого вопроса и ответа «что это такое», так что вот оно. Вот частичный список мест, где это было упомянуто ранее:
Ответы:
обзор
Зачем нам нужен способ копирования и обмена?
Любой класс, который управляет ресурсом ( обертка , как умный указатель), должен реализовать Большую тройку . В то время как цели и реализация конструктора и деструктора копирования просты, оператор присвоения копии, пожалуй, самый нюансированный и сложный. Как это должно быть сделано? Какие подводные камни следует избегать?
Копирования и замены идиома это решение, и элегантно помогает оператору присваивания в достижении двух целей: во избежание дублирования кода , и обеспечивая надежную гарантию исключения .
Как это работает?
Концептуально , он работает с использованием функциональности конструктора копирования для создания локальной копии данных, а затем берет скопированные данные с помощью
swap
функции, заменяя старые данные новыми данными. Затем временная копия разрушается, забирая старые данные. Нам остается копия новых данных.Чтобы использовать идиому копирования и замены, нам нужны три вещи: рабочий конструктор копирования, рабочий деструктор (оба являются основой любой оболочки, поэтому в любом случае должны быть завершены) и
swap
функция.Функция подкачки - это функция без выбрасывания, которая меняет два объекта класса, член на член. Мы могли бы соблазниться использовать
std::swap
вместо предоставления своих собственных, но это было бы невозможно;std::swap
использует конструктор копирования и оператор копирования-присваивания в своей реализации, и мы в конечном итоге попытаемся определить оператор присваивания в терминах самого себя!(Не только это, но и неквалифицированные вызовы
swap
будут использовать наш собственный оператор подкачки, пропуская ненужную конструкцию и разрушение нашего класса, которыеstd::swap
могут повлечь за собой.)Подробное объяснение
Цель
Давайте рассмотрим конкретный случай. Мы хотим управлять в другом бесполезном классе динамическим массивом. Начнем с рабочего конструктора, конструктора копирования и деструктора:
Этот класс почти успешно управляет массивом, но он должен
operator=
работать правильно.Неудачное решение
Вот как может выглядеть наивная реализация:
И мы говорим, что мы закончили; это теперь управляет массивом, без утечек. Тем не менее, он страдает от трех проблем, обозначенных последовательно в коде как
(n)
.Первый - это тест на самостоятельное назначение. Эта проверка служит двум целям: это простой способ запретить нам запускать ненужный код при самостоятельном назначении, и он защищает нас от незаметных ошибок (таких как удаление массива только для попытки его копирования). Но во всех остальных случаях это просто замедляет работу программы и действует как шум в коде; самопредставление происходит редко, поэтому большую часть времени эта проверка является пустой тратой. Было бы лучше, если бы оператор мог нормально работать без него.
Второе - это то, что он предоставляет только базовую гарантию исключения. Если
new int[mSize]
не удается,*this
будут изменены. (А именно, размер неправильный, а данные исчезли!) Для гарантии строгих исключений это должно быть чем-то вроде:Код расширился! Что приводит нас к третьей проблеме: дублирование кода. Наш оператор присваивания эффективно дублирует весь код, который мы уже написали в другом месте, и это ужасно.
В нашем случае ядро всего две строки (выделение и копирование), но с более сложными ресурсами это раздувание кода может быть довольно хлопотным. Мы должны стремиться никогда не повторяться.
(Можно задаться вопросом: если для правильного управления одним ресурсом требуется такой большой код, что если мой класс управляет более чем одним? Хотя это может показаться обоснованным, и на самом деле для этого требуются нетривиальные
try
/catch
предложения, это не Это потому, что класс должен управлять только одним ресурсом !)Успешное решение
Как уже упоминалось, идиома копирования и обмена исправит все эти проблемы. Но сейчас у нас есть все требования, кроме одного:
swap
функция. Хотя правило трех успешно влечет за собой существование нашего конструктора копирования, оператора присваивания и деструктора, его действительно следует называть «Большая тройка с половиной»: всякий раз, когда ваш класс управляет ресурсом, имеет смысл также предоставитьswap
функцию ,Нам нужно добавить функциональность подкачки в наш класс, и мы делаем это следующим образом †:
( Вот объяснение, почему
public friend swap
.) Теперь мы можем не только обменять нашиdumb_array
, но и вообщеон просто меняет указатели и размеры, а не выделяет и копирует целые массивы. Помимо этого бонуса в функциональности и эффективности, мы теперь готовы реализовать идиому копирования и замены.Без лишних слов наш оператор присваивания:
И это все! Одним махом все три проблемы решаются одновременно.
Почему это работает?
Сначала отметим важный выбор: аргумент параметра принимается по значению . Хотя можно так же легко сделать следующее (и действительно, многие наивные реализации этой идиомы делают):
Мы теряем важную возможность оптимизации . Не только это, но и этот выбор имеет решающее значение в C ++ 11, который будет обсуждаться позже. (В общем, замечательно полезный совет: если вы собираетесь сделать копию чего-либо в функции, пусть компилятор сделает это в списке параметров. ‡)
В любом случае, этот метод получения нашего ресурса является ключом к устранению дублирования кода: мы используем код из конструктора копирования для создания копии, и нам никогда не нужно повторять ее. Теперь, когда копия сделана, мы готовы поменяться.
Обратите внимание, что после входа в функцию все новые данные уже распределены, скопированы и готовы к использованию. Это то, что дает нам полную гарантию исключения бесплатно: мы даже не войдем в функцию, если построение копии не удастся, и поэтому невозможно изменить состояние
*this
. (То, что мы делали раньше вручную для гарантии исключений, сейчас делает для нас компилятор; как мило.)На данный момент мы свободны от дома, потому что
swap
не бросали. Мы заменяем наши текущие данные на скопированные, безопасно изменяя наше состояние, и старые данные помещаются во временные. Старые данные затем освобождаются, когда функция возвращается. (Где заканчивается область действия параметра и вызывается его деструктор.)Поскольку идиома не повторяет код, мы не можем вводить ошибки в операторе. Обратите внимание, что это означает, что мы избавляемся от необходимости проверки самоназначения, позволяющей единую единообразную реализацию
operator=
. (Кроме того, у нас больше нет штрафа за невыполнение заданий.)И это идиома копирования и обмена.
Как насчет C ++ 11?
Следующая версия C ++, C ++ 11, вносит одно очень важное изменение в то, как мы управляем ресурсами: Правило трех теперь является Правилом четырех (с половиной). Почему? Поскольку мы не только должны иметь возможность копировать-конструировать наш ресурс, нам также необходимо перемещать-конструировать его .
К счастью для нас, это легко:
Что тут происходит? Вспомните цель конструкции перемещения: взять ресурсы из другого экземпляра класса, оставив его в состоянии, гарантированно присваиваемом и разрушаемом.
Итак, что мы сделали, это просто: инициализировать с помощью конструктора по умолчанию (функция C ++ 11), затем поменять местами с
other
; мы знаем, что созданный по умолчанию экземпляр нашего класса можно безопасно назначать и уничтожать, поэтому мы знаемother
, что смогут сделать то же самое после замены.(Обратите внимание, что некоторые компиляторы не поддерживают делегирование конструктора; в этом случае мы должны вручную создать класс по умолчанию. Это неудачная, но, к счастью, тривиальная задача.)
Почему это работает?
Это единственное изменение, которое мы должны внести в наш класс, так почему это работает? Вспомните всегда важное решение, которое мы приняли, чтобы сделать параметр значением, а не ссылкой:
Теперь, если
other
инициализируется с помощью значения r, оно будет построено с ходом . Отлично. Таким же образом C ++ 03 позволяет нам повторно использовать нашу функцию конструктора копирования, принимая аргумент за значением, C ++ 11 автоматически выбирает конструктор перемещения, когда это уместно. (И, конечно, как упоминалось в ранее связанной статье, копирование / перемещение значения может быть просто полностью исключено.)И так завершает идиому копирования и обмена.
Сноски
* Почему мы устанавливаем
mArray
в ноль? Потому что, если какой-либо дополнительный код в операторе выдает,dumb_array
может быть вызван деструктор ; и если это происходит без установки значения null, мы пытаемся удалить уже удаленную память! Мы избегаем этого, устанавливая его в null, так как удаление null - это не операция.† Существуют и другие утверждения, что мы должны специализироваться
std::swap
для нашего типа, предоставлять в своем классеswap
наряду со свободной функциейswap
и т. Д. Но все это не нужно: любое правильное использованиеswap
будет осуществляться через неквалифицированный вызов, и наша функция будет нашел через ADL . Одна функция будет делать.‡ Причина проста: если у вас есть ресурс для себя, вы можете поменять его и / или переместить (C ++ 11) куда угодно. А сделав копию в списке параметров, вы максимально оптимизируете.
†† Обычно конструктор перемещения должен быть таким
noexcept
, в противном случае некоторый код (например,std::vector
логика изменения размера) будет использовать конструктор копирования, даже если перемещение имело бы смысл. Конечно, отметьте его только для случаев, когда код внутри не генерирует исключения.источник
swap
чтобы ваш файл находился во время ADL, если вы хотите, чтобы он работал в наиболее общем коде, с которым вы столкнетесь, например,boost::swap
и в других различных экземплярах подкачки. Своп - сложная проблема в C ++, и, как правило, мы все согласны с тем, что лучше всего использовать одну точку доступа (для согласованности), и единственный способ сделать это в общем случае - свободная функция (int
не может иметь члена подкачки, например). Смотрите мой вопрос для некоторого фона.Назначение в своей основе состоит из двух этапов: разрушение старого состояния объекта и построение его нового состояния как копии состояния какого-либо другого объекта.
По сути, это то, что делают деструктор и конструктор копирования , поэтому первая идея заключается в том, чтобы передать им работу. Однако, поскольку разрушение не должно завершиться неудачей, в то время как строительство может, мы действительно хотим сделать это наоборот : сначала выполнить конструктивную часть и, если это удалось, затем выполнить разрушительную часть . Идиома копирования и замены - это способ сделать это: сначала он вызывает конструктор копирования класса для создания временного объекта, затем обменивается данными с временным, а затем позволяет деструктору временного уничтожить старое состояние.
поскольку
swap()
предполагается, что он никогда не потерпит неудачу, единственная часть, которая может потерпеть неудачу, - это конструкция копирования. Это выполняется в первую очередь, и в случае неудачи ничего не будет изменено в целевом объекте.В своей уточненной форме копирование и замена реализованы путем выполнения копирования путем инициализации (не ссылочного) параметра оператора присваивания:
источник
std::swap(this_string, that)
не дает гарантии без броска. Это обеспечивает надежную исключительную безопасность, но не гарантирует отсутствие бросков.std::string::swap
которые могут быть вызваны (которые вызываютсяstd::swap
). В C ++ 0xstd::string::swap
естьnoexcept
и не должно вызывать исключения.std::array
...)Уже есть несколько хороших ответов. Я сосредоточусь в основном на том, что, как мне кажется, им не хватает - объяснение "минусов" с идиомой копирования и обмена ....
Способ реализации оператора присваивания в терминах функции подкачки:
Основная идея заключается в том, что:
наиболее подверженная ошибкам часть назначения объекта - обеспечение любых ресурсов, необходимых для нового состояния (например, память, дескрипторы)
это приобретение может быть предпринято до изменения текущего состояния объекта (то есть
*this
), если сделана копия нового значения, поэтомуrhs
оно принимается по значению (то есть копируется), а не по ссылкепоменять местами локальную копию
rhs
и,*this
как правило, это относительно легко сделать без потенциальных сбоев / исключений, поскольку локальная копия впоследствии не нуждается в каком-либо конкретном состоянии (просто требуется состояние, подходящее для запуска деструктора, так же как и для перемещаемого объекта из в> = C ++ 11)Если вы хотите, чтобы возражение против назначенного объекта не было затронуто назначением, которое выдает исключение, при условии, что у вас есть или может быть написано
swap
с сильной гарантией исключения, и в идеале такое, которое не может завершиться неудачей /throw
.. †Когда вам нужен простой, понятный и надежный способ определения оператора присваивания в терминах (более простого) конструктора копирования
swap
и функций деструктора.†
swap
throwing: как правило, можно надежно поменять элементы данных, которые объекты отслеживают по указателю, но элементы без указателя данных, которые не имеют swap без бросков или для которых обмен должен быть реализован какX tmp = lhs; lhs = rhs; rhs = tmp;
и конструкция копирования или присваивание может бросить, все еще может потерпеть неудачу, оставляя некоторые элементы данных замененными, а другие нет. Этот потенциал применим даже к C ++ 03std::string
, поскольку Джеймс комментирует другой ответ:‡ Реализация оператора присваивания, которая кажется разумной при назначении из отдельного объекта, может легко потерпеть неудачу для самостоятельного назначения. Хотя может показаться невообразимым, что клиентский код даже попытается выполнить самостоятельное назначение, это может сравнительно легко произойти во время операций algo над контейнерами с
x = f(x);
кодом, в которомf
(возможно, только для некоторых#ifdef
ветвей) есть макрос#define f(x) x
или функция, возвращающая ссылкуx
, или даже (вероятно, неэффективный, но сжатый) код, какx = c1 ? x * 2 : c2 ? x / 2 : x;
). Например:При самостоятельном назначении код удаления, приведенный выше
x.p_;
, указываетp_
на вновь выделенную область кучи, затем пытается прочитать неинициализированные в ней данные (Undefined Behavior), если это не делает ничего странного,copy
пытается выполнить самостоятельное назначение каждому просто уничтожено "Т"!I Идиома копирования и замены может привести к неэффективности или ограничениям из-за использования дополнительного временного параметра (когда параметр оператора создается методом копирования):
Здесь рукописный текст
Client::operator=
может проверять,*this
подключен ли он уже к тому же серверу, что иrhs
(возможно, посылать код «сброса», если это полезно), тогда как подход «копировать и менять» будет вызывать конструктор копирования, который, вероятно, будет записан для открытия. отличное соединение сокета затем закройте оригинал. Мало того, что это может означать удаленное сетевое взаимодействие вместо простой внутрипроцессной копии переменных, оно может нарушать ограничения клиента или сервера для ресурсов сокетов или соединений. (Конечно, у этого класса довольно неприятный интерфейс, но это другое дело ;-P).источник
Client
состоит в том, что присвоение не запрещено.Этот ответ больше похож на дополнение и небольшую модификацию ответов выше.
В некоторых версиях Visual Studio (и, возможно, в других компиляторах) есть ошибка, которая действительно раздражает и не имеет смысла. Так что если вы объявите / определите свою
swap
функцию следующим образом:... компилятор будет кричать на вас, когда вы вызываете
swap
функцию:Это как-то связано с
friend
вызываемой функцией иthis
передачей объекта в качестве параметра.Способ обойти это - не использовать
friend
ключевое слово и переопределитьswap
функцию:На этот раз вы можете просто позвонить
swap
и пройтиother
, сделав таким образом счастливым компилятор:В конце концов, вам не нужно использовать
friend
функцию, чтобы поменять 2 объекта. Также имеет смысл создатьswap
функцию-член, в которойother
в качестве параметра используется один объект.У вас уже есть доступ к
this
объекту, поэтому передача его в качестве параметра технически избыточна.источник
friend
функция вызывается с*this
параметромЯ хотел бы добавить слово предупреждения, когда вы имеете дело с контейнерами, поддерживающими распределитель в стиле C ++ 11. Обмен и назначение имеют слегка различную семантику.
Для конкретности, давайте рассмотрим контейнер
std::vector<T, A>
, гдеA
есть некоторый тип распределителя с сохранением состояния, и мы сравним следующие функции:Цель обеих функций
fs
иfm
состоит в том, чтобы датьa
состояние, котороеb
имело изначально. Тем не менее, есть скрытый вопрос: что произойдет, еслиa.get_allocator() != b.get_allocator()
? Ответ: это зависит. Давай напишемAT = std::allocator_traits<A>
.Если
AT::propagate_on_container_move_assignment
естьstd::true_type
, тоfm
переназначает распределитель значенияa
со значениемb.get_allocator()
, в противном случае это не так, иa
продолжает использовать свой исходный распределитель. В этом случае элементы данных необходимо поменять местами по отдельности, поскольку хранениеa
иb
несовместимо.Если
AT::propagate_on_container_swap
это такstd::true_type
, то происходитfs
обмен данными и распределителями ожидаемым образом.Если
AT::propagate_on_container_swap
естьstd::false_type
, то нам нужна динамическая проверка.a.get_allocator() == b.get_allocator()
, тогда два контейнера используют совместимое хранилище, и замена происходит обычным образом.a.get_allocator() != b.get_allocator()
программа имеет неопределенное поведение (см. [Container.requirements.general / 8].В результате подкачка стала нетривиальной операцией в C ++ 11, как только ваш контейнер начинает поддерживать распределители с сохранением состояния. Это несколько «продвинутый вариант использования», но он не совсем маловероятен, поскольку оптимизация перемещений обычно становится интересной только тогда, когда ваш класс управляет ресурсом, а память является одним из самых популярных ресурсов.
источник