Что такое идиома копирования и обмена?

Что это за идиома и когда ее следует использовать? Какие проблемы решает? Меняется ли идиома при использовании C ++ 11?

Хотя об этом упоминалось во многих местах, у нас не было единственного вопроса и ответа «что это такое», так что вот он. Вот неполный список мест, где это упоминалось ранее:


person GManNickG    schedule 19.07.2010    source источник
comment
gotw.ca/gotw/059.htm от Херба Саттера   -  person DumbCoder    schedule 19.07.2010
comment
Замечательно, я связал этот вопрос со своим ответом на перемещение семантики..   -  person fredoverflow    schedule 19.07.2010
comment
Хорошая идея получить полноценное объяснение этой идиомы, она настолько распространена, что о ней должен знать каждый.   -  person Matthieu M.    schedule 19.07.2010
comment
Предупреждение: идиома копирования / обмена используется гораздо чаще, чем полезна. Когда при назначении копии не требуется строгая гарантия безопасности исключений, это часто вредно для производительности. А когда для присваивания копий требуется строгая безопасность исключений, это легко обеспечивается короткой универсальной функцией в дополнение к гораздо более быстрому оператору присваивания копий. См. Слайды с 43 по 53 slideshare.net/ripplelabs/howard-hinnant-accu2014 Резюме: копирование / замена - полезный инструмент в наборе инструментов. Но он был перепродан, и впоследствии им часто злоупотребляли.   -  person Howard Hinnant    schedule 16.03.2016
comment
@HowardHinnant: Да, +1 к этому. Я написал это в то время, когда почти каждый вопрос C ++ помогал моему классу вылетать при его копировании, и это был мой ответ. Это уместно, когда вам просто нужна рабочая семантика копирования / перемещения или что-то еще, чтобы вы могли перейти к другим вещам, но это не совсем оптимально. Не стесняйтесь помещать отказ от ответственности в верхней части моего ответа, если вы думаете, что это поможет.   -  person GManNickG    schedule 16.03.2016
comment
Спасибо @GManNickG. Теперь видеопрезентация этих слайдов доступна здесь: youtube.com/watch?v= vLinb2fgkHk & t = 35 мин. 30 сек.   -  person Howard Hinnant    schedule 24.11.2016


Ответы (5)


Обзор

Зачем нам нужна идиома копирования и обмена?

Любой класс, который управляет ресурсом (оболочка, например интеллектуальный указатель), должен реализовать Большая тройка. В то время как цели и реализация копирующего конструктора и деструктора просты, оператор копирования-присваивания, возможно, является наиболее тонким и сложным. Как это сделать? Каких подводных камней нужно избегать?

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

Как это работает?

Концептуально он работает с использованием конструктора копирования функциональность для создания локальной копии данных, затем берет скопированные данные с помощью функции swap, заменяя старые данные новыми данными. Затем временная копия разрушается, забирая с собой старые данные. У нас осталась копия новых данных.

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

Функция подкачки - это не вызывающая функция, которая меняет местами два объекта класса, член на член. У нас может возникнуть соблазн использовать std::swap вместо того, чтобы предоставлять свои собственные, но это было бы невозможно; std::swap использует в своей реализации конструктор копирования и оператор присваивания копии, и в конечном итоге мы попытаемся определить оператор присваивания в терминах самого себя!

(Не только это, но и неквалифицированные вызовы swap будут использовать наш пользовательский оператор подкачки, пропуская ненужное построение и разрушение нашего класса, которое повлечет за собой std::swap.)


Подробное объяснение

Цель

Рассмотрим конкретный случай. Мы хотим управлять динамическим массивом в бесполезном в противном случае классе. Начнем с рабочего конструктора, конструктора копирования и деструктора:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr)
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Этот класс почти успешно управляет массивом, но для правильной работы ему требуется operator=.

Неудачное решение

Вот как может выглядеть наивная реализация:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

И мы говорим, что закончили; это теперь управляет массивом без утечек. Однако он страдает от трех проблем, последовательно отмеченных в коде как (n).

  1. Первый - это тест самоназначения.
    Эта проверка служит двум целям: это простой способ предотвратить запуск ненужного кода при самоназначении и защищает нас от мелких ошибок (таких как удаление массива только для попробуйте скопировать). Но во всех остальных случаях это просто служит для замедления программы и действует как шум в коде; Самостоятельное присвоение происходит редко, поэтому в большинстве случаев эта проверка бесполезна.
    Было бы лучше, если бы оператор мог нормально работать без этого.

  2. Во-вторых, он обеспечивает только базовую гарантию исключения. Если new int[mSize] не удается, *this будет изменен. (А именно, размер неправильный и данные пропали!)
    Для надежной гарантии исключения это должно быть что-то вроде:

     dumb_array& operator=(const dumb_array& other)
     {
         if (this != &other) // (1)
         {
             // get the new data ready before we replace the old
             std::size_t newSize = other.mSize;
             int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
             std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
             // replace the old data (all are non-throwing)
             delete [] mArray;
             mSize = newSize;
             mArray = newArray;
         }
    
         return *this;
     }
    
  3. Код расширился! Это приводит нас к третьей проблеме: дублированию кода.

Наш оператор присваивания эффективно дублирует весь код, который мы уже написали где-то еще, и это ужасно.

В нашем случае его ядро ​​составляет всего две строки (выделение и копия), но с более сложными ресурсами это раздувание кода может быть довольно неприятным. Мы должны стремиться никогда не повторяться.

(Кто-то может задаться вопросом: если для правильного управления одним ресурсом требуется столько кода, что, если мой класс управляет более чем одним?
Хотя это может показаться серьезной проблемой, и на самом деле это требует нетривиального _14 _ / _ 15_ пунктов, это не проблема.
Это потому, что класс должен управлять только одним ресурсом !)

Удачное решение

Как уже упоминалось, идиома копирования и замены решит все эти проблемы. Но прямо сейчас у нас есть все требования, кроме одного: swap функция. Хотя Правило трех успешно влечет за собой существование нашего конструктора копирования, оператора присваивания и деструктора, его действительно следует называть Большой тройкой с половиной: каждый раз, когда ваш класс управляет ресурсом, также имеет смысл предоставить функцию swap.

Нам нужно добавить в наш класс функцию подкачки, и мы делаем это следующим образом †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

(Вот объяснение, почему public friend swap.) Теперь мы можем не только поменять местами наши dumb_array, но свопы в целом могут быть более эффективными; он просто меняет местами указатели и размеры, а не выделяет и копирует целые массивы. Помимо этого бонуса в функциональности и эффективности, теперь мы готовы реализовать идиому копирования и обмена.

Без лишних слов наш оператор присваивания:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

Вот и все! Одним махом можно элегантно решить сразу все три проблемы.

Почему это работает?

Сначала мы замечаем важный выбор: аргумент параметра берется по значению. Хотя с таким же успехом можно было бы сделать следующее (как и многие наивные реализации этой идиомы):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Мы теряем важная возможность оптимизации. Не только это, но и этот выбор критически важен для C ++ 11, который обсуждается позже. (В общем, замечательно полезное руководство выглядит следующим образом: если вы собираетесь сделать копию чего-либо в функции, позвольте компилятору сделать это в списке параметров. ‡)

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

Обратите внимание, что при входе в функцию все новые данные уже размещены, скопированы и готовы к использованию. Это то, что дает нам надежную бесплатную гарантию исключения: мы даже не войдем в функцию, если построение копии завершится неудачно, и, следовательно, невозможно изменить состояние *this. (То, что мы раньше делали вручную для надежной гарантии исключения, теперь делает за нас компилятор; как любезно.)

На данный момент мы находимся без дома, потому что swap не бросает. Мы меняем наши текущие данные на скопированные, безопасно изменяя свое состояние, а старые данные помещаются во временные. После возврата из функции старые данные удаляются. (Где заканчивается область действия параметра и вызывается его деструктор.)

Поскольку идиома не повторяет код, мы не можем вносить ошибки в оператор. Обратите внимание, что это означает, что мы избавляемся от необходимости проверки самостоятельного назначения, что позволяет использовать единую унифицированную реализацию operator=. (Кроме того, у нас больше нет штрафа за выполнение заданий, не выполняемых самостоятельно.)

И это идиома копирования и обмена.

А как насчет C ++ 11?

Следующая версия C ++, C ++ 11, вносит одно очень важное изменение в способ управления ресурсами: Правило трех теперь называется Правило четырех (с половиной). Почему? Потому что нам не только нужно иметь возможность копировать-создавать наш ресурс, нам нужно переместить-построить его тоже.

К счастью для нас, это легко:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Что тут происходит? Вспомните цель move-Construction: взять ресурсы из другого экземпляра класса, оставив его в состоянии, которое гарантированно может быть присвоено и разрушаемо.

Итак, то, что мы сделали, очень просто: инициализировать с помощью конструктора по умолчанию (функция C ++ 11), а затем поменять местами с other; мы знаем, что созданный по умолчанию экземпляр нашего класса может быть безопасно назначен и уничтожен, поэтому мы знаем, что other сможет сделать то же самое после замены.

(Обратите внимание, что некоторые компиляторы не поддерживают делегирование конструктора; в этом случае мы должны вручную создать класс по умолчанию. Это неудачная, но, к счастью, тривиальная задача.)

Почему это работает?

Это единственное изменение, которое нам нужно внести в наш класс, так почему это работает? Помните важное решение, которое мы приняли, чтобы сделать параметр значением, а не ссылкой:

dumb_array& operator=(dumb_array other); // (1)

Теперь, если other инициализируется rvalue, он будет сконструирован с перемещением. Идеально. Точно так же, как C ++ 03 позволяет нам повторно использовать наши функции конструктора копирования, принимая аргумент по значению, C ++ 11 автоматически выбирает конструктор перемещения, когда это необходимо. (И, конечно, как упоминалось в ранее связанной статье, копирование / перемещение значения можно просто полностью исключить.)

На этом заканчивается идиома копирования и обмена.


Сноски

* Почему мы устанавливаем mArray равным нулю? Потому что, если какой-либо следующий код в операторе выдает ошибку, может быть вызван деструктор dumb_array; и если это произойдет, не установив для него значение null, мы попытаемся удалить уже удаленную память! Мы избегаем этого, установив для него значение null, поскольку удаление null не является операцией.

† Есть и другие утверждения о том, что мы должны специализировать std::swap для нашего типа, предоставить swap внутри класса наряду со свободной функцией swap и т. Д. Но это все ненужно: любое правильное использование swap будет осуществляться через неквалифицированный вызов, и нашу функцию можно будет найти через ADL. Подойдет одна функция.

‡ Причина проста: если у вас есть ресурс, вы можете поменять его местами и / или переместить (C ++ 11) куда угодно. А сделав копию в списке параметров, вы максимизируете оптимизацию.

† † Конструктор перемещения обычно должен быть noexcept, в противном случае некоторый код (например, std::vector логика изменения размера) будет использовать конструктор копирования, даже если перемещение будет иметь смысл. Конечно, пометьте его как noexcept, только если код внутри не генерирует исключения.

person GManNickG    schedule 19.07.2010
comment
@GMan: пользовательская перегрузка подкачки все еще потенциально быстрее, потому что семантика перемещения должна устанавливать указатели источника на ноль после их копирования. - person fredoverflow; 19.07.2010
comment
@Fred: Оптимизирующий компилятор может легко увидеть, что такое назначение расточительно. Я демонстрирую это в своем ответе, на который я ссылался в сообщении. Однако вы можете быть уверены в том, что без таких вещей swap все равно мог бы быть быстрее. Хотя я бы не стал считать, что это того стоит больше. - person GManNickG; 19.07.2010
comment
@GMan: Если мы полагаемся на семантику перемещения, классу определенно нужен объект перемещения и оператор присваивания перемещения. Так что на самом деле здесь больше требований, чем у вашего собственного swap(). (Я не возражаю против этого, просто об этом следует упомянуть.) - person sbi; 19.07.2010
comment
@sbi: Да, пока реализуется семантика перемещения .. :) Если я не понимаю. (РЕДАКТИРОВАТЬ: Думаю, нет.: P) Тем не менее, я сделаю раздел C ++ 0x более конкретным. Кстати, я думаю, что требование dtor явно выражается, когда я говорю, что это для классов, реализующих Большую тройку. Я посмотрю, смогу ли я это украсть. - person GManNickG; 19.07.2010
comment
@GMan: Я бы сказал, что класс, управляющий несколькими ресурсами одновременно, обречен на неудачу (безопасность исключений становится кошмаром), и я настоятельно рекомендую либо класс управлять ОДНИМ ресурсом, либо он имеет бизнес-функции и использует менеджеров. - person Matthieu M.; 19.07.2010
comment
@GMan хороший :) Интересно, почему ты проводишь этот тест с нулевым размером? Массивы с динамическим нулевым размером допустимы. - person Johannes Schaub - litb; 19.07.2010
comment
@Johannes: Я знаю, что с динамическими массивами нулевого размера все в порядке, но я проверил: 1) избежать необходимости использовать динамическую память и 2) сделать их более сложными. : P И хорошо знать, что этого достаточно, я должен признать, я был немного сбит с толку, о чем он говорил. - person GManNickG; 19.07.2010
comment
@Matthieu: Я согласен, я добавил это. @Johannes: Я немного подправил это в отношении самостоятельного назначения. - person GManNickG; 20.07.2010
comment
Очень красивое резюме! Я бы лично прокомментировал throw(). Оставьте текст здесь, чтобы указать, что вы не думаете, что функция будет вызывать, но оставьте возможные штрафы: boost.org/development/requirements.html#Exception-specification - person Bill; 26.08.2010
comment
@GMan: или вы используете квалификатор C ++ 0x nothrow :-) - person Matthieu M.; 21.09.2010
comment
@Chubsdad: Спасибо. Удачное время, я планировал (и только что сделал) улучшить и расширить изменения C ++ 0x. - person GManNickG; 29.09.2010
comment
Большой четверки? (1) dtor, (2) скопировать ctor, (3) скопировать op =, (4) переместить ctor и (5) переместить op =. Что из этого не учитывается? - person James McNellis; 02.10.2010
comment
@ Джеймс: Есть только один оператор присваивания. - person GManNickG; 02.10.2010
comment
@GMan: Вы можете объявить и копирование op = и перемещение op =, не так ли? (Однако неявное перемещение op = подавляется, если реализовано копирование op =). Или, как я понимаю. Почему я не прав? - person James McNellis; 02.10.2010
comment
@ Джеймс: Нет, ты прав, можешь. Вы просто берете его по значению как в C ++ 03, так и в C ++ 0x. (В C ++ 03 lvalue копируются, rvalues, надеюсь, удаляется из своей копии, в C ++ 0x lvalues ​​копируются, ravlues перемещаются, а иногда, надеюсь, опускаются). - person GManNickG; 02.10.2010
comment
@GMan: просто перечитайте эту статью (часть C ++ 0x), и я подумал о Большой четверке. Я бы сказал, что Большая пятерка включает в себя Оператор присвоения перемещения: P Определяется ли Оператор присваивания перемещения автоматически, если вы его не объявляете? - person Matthieu M.; 16.11.2010
comment
@GMan: C ++ 0x будет поддерживать неявное перемещение, хотя и с некоторыми ограничениями. (Нам придется подождать до следующей рассылки в конце месяца, чтобы узнать, какие ограничения были согласованы ...) - person James McNellis; 17.11.2010
comment
@GMan, спасибо за очень хороший урок! не могли бы вы привести пример того, как неперегруженный метод copy будет работать с использованием идиомы копирования и обмена, например, такой метод, как void copy(const dumb_array& other)? - person Javier; 15.06.2011
comment
@ Хавьер: Я не уверен, что понимаю, что вы имеете в виду. Может быть, вы можете задать полный вопрос, если нужно, обязательно включите примеры кода. - person GManNickG; 15.06.2011
comment
@GMan, будет ли приведенный ниже метод концептуально правильным в соответствии с идиомой копирования и обмена? void dumb_array::copy(dumb_array other) { swap(*this, other); } - person Javier; 15.06.2011
comment
@Javier: Идиома полезна для реализации необходимых функций для семантики копирования (большая тройка). Думаю, мой ответ - да, но я не вижу применения такой функции. - person GManNickG; 15.06.2011
comment
@ Хавьер: Я не могу вспомнить случай, когда мне нужно было бы скопировать только определенных участников. Возможно, эти члены должны быть их собственным классом со своими функциями. - person GManNickG; 15.06.2011
comment
Тема, которую можно было бы часами читать по всему Интернету, теперь помещена в объяснение Single Compact Complete and Perfect (c). Как добрый @GMan! +1 - person jweyrich; 27.06.2011
comment
Что, если конструктором dumb_array является explicit? Позвонить operator=(dumb_array other) не получится, правда? - person Gabriel; 03.11.2011
comment
@ Габриэль: Как это назвать? Если бы это было, скажем, dumb_array x(1); x = 5;, тогда нет, это не сработало бы. Но на самом деле это не имеет отношения ни к чему, кроме явного конструктора. - person GManNickG; 03.11.2011
comment
@GMan Хорошо, тогда это означает, что если классу требуется, чтобы его конструктор был явным по какой-либо причине, он не может использовать эту реализацию идиомы копирования и обмена. Небольшое изменение в operator= исправит это: передача копируемого объекта по ссылке и объявление локального объекта в начале функции, его копирование. - person Gabriel; 03.11.2011
comment
@ Габриэль: Я не уверен, что понимаю. explicit конструкторы и копирование и замена - две совершенно не связанные между собой вещи. Изменение параметра на ссылку по-прежнему не позволит коду из моего предыдущего примера работать. - person GManNickG; 03.11.2011
comment
@GMan Извините, я говорил о явном конструкторе copy. Как вы это выразились, если вам нужно установить конструктор копирования dumb_array как явный, вы не сможете написать: dumb_array a, b; a = b;. Измените конструктор копирования на dumb_array& operator=(dumb_array & other) { swap(*this, dumb_array(other)); return *this; }, и теперь вы можете. - person Gabriel; 04.11.2011
comment
@ Габриэль: Да, конечно. Тем не менее, я никогда не находил веского оправдания для явного создания конструктора копирования. - person GManNickG; 04.11.2011
comment
Я не понимаю, почему метод подкачки объявлен здесь как друг? - person szx; 13.12.2011
comment
@asd: чтобы его можно было найти через ADL. - person GManNickG; 13.12.2011
comment
И я не понимаю, почему конструктор по умолчанию выполняет new int [mSize] (), а конструктор копирования - new int [mSize]. Какая разница? - person neuviemeporte; 19.07.2012
comment
@neuviemeporte: в скобках элементы массива инициализируются по умолчанию. Без них они не инициализированы. Поскольку в конструкторе копирования мы все равно будем перезаписывать значения, мы можем пропустить инициализацию. - person GManNickG; 19.07.2012
comment
@GMan: Спасибо. Кроме того, мне было интересно, может ли swap () быть функцией-членом dumb_array. Функции глобального друга кажутся мне беспорядочными. - person neuviemeporte; 19.07.2012
comment
@neuviemeporte: если вы хотите, чтобы его нашли во время ADL (using std::swap; swap(x, y);), он должен быть глобальным другом. - person GManNickG; 19.07.2012
comment
@GMan: Я не уверен, что понимаю; Разве создание swap () члена не упростило бы задачу, чтобы вам вообще не нужно было заботиться об ADL? - person neuviemeporte; 20.07.2012
comment
@neuviemeporte: вам нужно, чтобы ваш swap был найден во время ADL, если вы хотите, чтобы он работал в большинстве общих кодов, с которыми вы столкнетесь, например boost::swap и других различных экземплярах подкачки. Своп - сложная проблема в C ++, и, как правило, все мы пришли к единому мнению, что лучше всего использовать одну точку доступа (для согласованности), и единственный способ сделать это в целом - это бесплатная функция (int не может иметь члена подкачки , Например). См. мой вопрос для некоторой предыстории. - person GManNickG; 20.07.2012
comment
Как вы делаете swap для производного класса? - person Kerrek SB; 07.09.2012
comment
@KerrekSB: Звучит как разумный вопрос сам по себе. Признаюсь, я не уверен, что полностью понимаю, какой вариант использования вам нужен. - person GManNickG; 07.09.2012
comment
@GManNickG: Непосредственной мотивацией является эта проблема, но я недавно вообще задавался вопросом, можно ли написать своп для производный класс путем нарезки при условии, что база обеспечивает подкачку. - person Kerrek SB; 07.09.2012
comment
@KerrekSB: Я бы просто использовал тот же подход, что и вы, поменял местами базовые классы перед тем, как поменять местами самый производный класс. - person GManNickG; 07.09.2012
comment
@GManNickG: Я пришел к выводу, что нарезка существует именно с целью реализации конструктора копирования, назначения копирования и функций подкачки для производных классов. Своп был добавлен как раз сегодня, когда меня осенило. Звучит разумно? - person Kerrek SB; 07.09.2012
comment
@KerrekSB: На самом деле я не вижу нарезки. Насколько я понимаю, A::swap(rhs) это то же самое, что this->A::swap(rhs); если A::swap вызывает какие-либо виртуальные функции, например (а не должно), они могут быть отправлены в самый производный класс. - person GManNickG; 07.09.2012
comment
@GManNickG: Хм, хороший момент; вызов базы swap на самом деле не срезает. Но если предположить, что каждый класс реализует свою собственную стандартную семантику подкачки, он меняет местами только базовую часть объекта. - person Kerrek SB; 07.09.2012
comment
@GManNickG Ваш конструктор перемещения зависит от ctor по умолчанию. Означает ли это, что (с учетом семантики перемещения) каждая оболочка ресурса обязательно должна иметь 1) допустимое пустое состояние и 2) ctor по умолчанию, который инициализируется этим состоянием? - person Kos; 23.12.2012
comment
@Kos: Технически нет. Вы можете делать все, что хотите, в своем конструкторе перемещения и иметь любое состояние перемещения. Но это более чем хорошая практика - вернуться в простое многократно используемое состояние. Классы стандартной библиотеки (например, std::vector<>) делают это. - person GManNickG; 23.12.2012
comment
@GManNickG В предыдущем комментарии вы сказали, что swap должен быть глобальным другом, чтобы его можно было найти через ADL. В вашем примере swap выглядит как открытый член dumb_array. Что мне не хватает? - person zmb; 12.06.2013
comment
@zmb: глобальная часть (то есть объявленная полностью вне класса) не является обязательной. Важно то, что это дружественная функция, не являющаяся членом, поэтому ее можно будет найти во время ADL, например, когда стандартная библиотека вызывает swap(x, y);. См. this для получения более полной информации. - person GManNickG; 12.06.2013
comment
Будет ли рекомендация о dumb_array(dumb_array&& other) (т.е. конструкция по умолчанию, а затем свопинг) будет другой, если dumb_array::dumb_array() будет дорогостоящим (скажем, всегда зарезервировано место)? Было бы лучше в этом случае инициализировать элементы, перемещаясь от другого объекта, а затем обнулить состояние перемещенного объекта? - person Ben Hymers; 30.07.2013
comment
@BenHymers: Да. Идиома копирования и обмена предназначена только для упрощения создания новых классов управления ресурсами в общих чертах. Для каждого конкретного класса почти наверняка существует более эффективный маршрут. Эта идиома просто работает, и ее трудно сделать неправильно. - person GManNickG; 30.07.2013
comment
@GManNickG, ваш ответ полезен, но вы должны отдать должное [icce.rug .nl / documents / cplusplus / cplusplus11.html # l194] [icce.rug.nl/documents/cplusplus/cplusplus09.html#l159], Скотт Мейерс не навредит - person Nikolaos Giotis; 29.09.2013
comment
@GManNickG: FWIW, такие утверждения, как dumb_array temp (другое); несостоятельны в универсальном коде (например, в векторной реализации STL). Причина в том, что размер dumb_array является произвольным и может быть слишком большим для стека и вызывать сбой приложения из-за нехватки места в стеке. Я сталкивался с этой ситуацией несколько раз на практике, особенно на платформах с небольшими стеками по умолчанию, такими как Mac OS X. - person ThreeBit; 24.11.2013
comment
@ThreeBit: массив не сохраняется в пространстве стека, как и в std::vector с распределителем по умолчанию. На практике вы столкнулись с другой проблемой. - person GManNickG; 24.11.2013
comment
@GManNickG Вы правы насчет вектора, так как почти все реализации вектора размещают свою память в куче. Я не должен был использовать это как предполагаемый пример. Я имел в виду, что в целом вы не можете написать общий код, который создает экземпляры произвольных объектов в стеке. Конечно, есть альтернативы. - person ThreeBit; 25.11.2013
comment
Мой случай - необычный, но я столкнулся с тонкой проблемой при использовании std :: swap в классе, который имеет указатель на строку, являющуюся частью объекта. Обсуждается здесь: stackoverflow.com/a/20513948/522385 - person Chap; 12.12.2013
comment
Почему эта функция подкачки объявлена ​​как friend в этой строке friend void swap(dumb_array& first, dumb_array& second) // nothrow? если он объявлен внутри класса, разве это не просто функция-член? - person Vis Viva; 08.04.2014
comment
@VisViva друг в классе не является функцией-членом в окружающей области видимости, но как друг он получает обычный доступ к непубличным членам. Это стилистический выбор, но он может быть довольно кратким, чтобы определить небольшие функции там, а не объявлять их и повторять подпись (возможно, с более явными параметрами шаблона) в окружающей области перед запуском в теле функции. - person Tony Delroy; 16.04.2014
comment
@TonyD, но почему он определен в самом классе? Разве это не должно быть написано где-то еще, а вместо этого просто объявлено внутри класса с ключевым словом friend? - person Vis Viva; 16.04.2014
comment
@VisViva, это всего лишь проблема стиля - краткость и четкое участие в общем интерфейсе класса по сравнению с отделением интерфейса от реализации (и, возможно, избежание неявной встроенности) - выбирайте, как вам нравится. - person Tony Delroy; 16.04.2014
comment
Реализация присваивания через конструктор копирования может вызвать ненужное выделение памяти, что может даже вызвать нежелательную ошибку нехватки памяти. Рассмотрим случай назначения 700 МБ dumb_array 1 ГБ dumb_aaray на машине с пределом кучи ‹2 ГБ. Оптимальное назначение позволит понять, что у него уже достаточно выделенной памяти, и скопировать данные только в уже выделенный буфер. Ваша реализация вызовет выделение еще одного буфера 700 МБ до того, как будет освобожден 1 ГБ, в результате чего все 3 будут пытаться сосуществовать в памяти одновременно, что без надобности вызовет ошибку нехватки памяти. - person Baruch; 27.05.2014
comment
Как это неоднократно повторяется, суть в том, чтобы что-то работало, а не было оптимальным. Существует компромисс между временем разработки и временем приложения. - person GManNickG; 30.05.2014
comment
@GManNickG: поскольку он жертвует эффективностью ради удобства обслуживания, вам, вероятно, следует явно указать где-нибудь в вопросе или ответе, что копирование и замена - это решение, а не решение, и что может иметь смысл повторно использовать объект, а не создавать новый. Иначе люди не догадываются. (Это кажется очевидным, но это не так; мне потребовалось много времени, чтобы осознать это.) - person user541686; 08.08.2014
comment
Для тех, кто задается вопросом, зачем использовать неквалифицированные свопы: std :: swap обычно вызывает конструктор копирования (конструктор перемещения в C ++ 11 +) и два оператора присваивания, а для классов, использующих идиому копирования и обмена, каждый оператор присваивания будет в любом случае вызвать специфичную для класса функцию подкачки. Следовательно, использование std :: swap будет стоить вдвое дороже, ПЛЮС стоимость конструктора перемещения или копирования. Это не значит, что копирование и замена делает std :: swap сверхмедленным; std :: swap просто неоптимален. - person Mike S; 21.08.2014
comment
По поводу компромиссов: я протестировал класс, подобный приведенному выше, с использованием трех версий: оператора старого стиля с тестом самоназначения, копированием и заменой и чуть более ручным копированием и перемещением, которое только меняет местами ресурсы и перемещает -записывает все остальное. Для пустого массива копирование и замена примерно на 25% быстрее, чем присваивание в старом стиле с помощью Clang, но для непустых массивов или для GCC присваивание в старом стиле примерно на 0–12,5% быстрее. Копирование и перемещение обычно сокращает время копирования и замены в крошечной доле, но не выигрывает от любой оптимизации, которую Clang использовал выше. (продолжение) - person Mike S; 21.08.2014
comment
Я надеялся, что отказ от теста самоназначения окупит дополнительные операции копирования и обмена, но, похоже, это не всегда так (вероятно, из-за быстрого предсказания ветвлений). Тем не менее, он близок и иногда выигрывает в зависимости от обстоятельств, поэтому вам не нужно много платить за шаблонную правильность и безопасность исключений, по крайней мере, для этого конкретного класса. Однако, по словам Говарда Хиннанта, идиома может быть до 8 раз медленнее для std :: vector, поэтому YMMV. - person Mike S; 21.08.2014
comment
@GManNickG, почему бы вам не попасть в большую пятерку с помощью оператора присваивания ходов? или он больше не нужен с данной реализацией оператора присваивания? Это не ясно ни из комментариев, ни из текста, не могли бы вы добавить абзац об этом, что было бы здорово! Благодарность :) - person user1708860; 18.10.2014

Назначение, по сути, состоит из двух этапов: разрушение старого состояния объекта и создание его нового состояния в виде копии состояния другого объекта.

По сути, это то, что делают деструктор и конструктор копирования, поэтому первая идея - делегировать работу им. Однако, поскольку разрушение не должно терпеть неудачу, а конструкция может, мы действительно хотим сделать это наоборот: сначала выполнить конструктивную часть и, если это удалось, затем выполнить деструктивную часть. Идиома копирования и замены - это способ сделать это: сначала он вызывает конструктор копирования класса для создания временного объекта, затем меняет его данные на временные, а затем позволяет временному деструктору разрушить старое состояние.
Поскольку предполагается, что swap() никогда не потерпит неудачу, единственная часть, которая может потерпеть неудачу, - это конструкция копирования. Это выполняется в первую очередь, и в случае сбоя в целевом объекте ничего не изменится.

В своей усовершенствованной форме копирование и замена реализуется путем выполнения копирования путем инициализации (не ссылочного) параметра оператора присваивания:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
person sbi    schedule 19.07.2010
comment
Я думаю, что упоминание pimpl так же важно, как упоминание копии, обмена и уничтожения. Обмен не является безопасным для магических исключений. Это безопасно для исключений, потому что замена указателей безопасна в отношении исключений. Вам не нужно использовать сутенера, но если вы этого не сделаете, вы должны убедиться, что каждый обмен участника безопасен в отношении исключений. Это может быть кошмаром, когда эти участники могут измениться, и тривиально, когда они прячутся за сутенером. А потом идет цена сутенера. Это приводит нас к выводу, что часто безопасность исключений требует снижения производительности. - person wilhelmtell; 22.12.2010
comment
... вы можете написать распределители для класса, которые будут поддерживать амортизацию затрат сутенера. Это добавляет сложности, которая упирается в простоту идиомы простого копирования и обмена. Это выбор. - person wilhelmtell; 22.12.2010
comment
std::swap(this_string, that) не дает гарантии отсутствия броска. Он обеспечивает надежную защиту от исключений, но не гарантирует отсутствие исключения. - person wilhelmtell; 22.12.2010
comment
Это означает, что если у вас есть два члена class-type, то в операторе присваивания, если первый обмен сработал, а второй не прошел, вам нужно будет убедиться, что вы отменили первый обмен, чтобы сохранить безопасность строгих исключений. И это всего с двумя членами class-type. - person wilhelmtell; 22.12.2010
comment
@wilhelmtell: Я сомневаюсь, что std::swap() нарушает гарантию отсутствия выброса при создании экземпляра с std::string. (Этот +1 в вашем комментарии был вызван тем, что я не смог щелкнуть мышью.) Существует специализация для std::string вызова std::string::swap(). [C ++ 03, 21.3.7.8: Эффект: lhs.swap(rhs);] - person sbi; 22.12.2010
comment
@wilhelmtell: В C ++ 03 нет упоминания об исключениях, которые могут быть вызваны std::string::swap (который вызывается std::swap). В C ++ 0x std::string::swap равно noexcept и не должно вызывать исключений. - person James McNellis; 22.12.2010
comment
@sbi @JamesMcNellis в порядке, но суть остается неизменной: если у вас есть члены класса-типа, вы должны убедиться, что их замена не является проблемой. Если у вас есть единственный член, который является указателем, то это тривиально. В противном случае это не так. - person wilhelmtell; 22.12.2010
comment
@wilhelmtell: Да. Я удивлен, что новая концепция C ++ 0x Swappable не требует, чтобы определяемые пользователем swap функции были noexcept. - person James McNellis; 22.12.2010
comment
@wilhelmtell: Я думал, что в этом смысл подкачки: он никогда не бросает и всегда O (1) (да, я знаю, _1 _...) - person sbi; 22.12.2010
comment
@sbi, что, если бы мы хотели использовать copy method, который по функциям похож на operator =. Например, такой метод, как void copy(const T & other). Как в этом случае можно использовать идиому копирования и обмена? - person Javier; 15.06.2011
comment
@ Хавьер: Что бы сделал этот метод? - person sbi; 15.06.2011
comment
@sbi, в случае перегрузки operator= мы копируем все члены класса. В моем случае мне нужно скопировать только определенные члены класса, и я хотел использовать для этого протокол copy-swap, например void myClass<T>::copy(myclass<T> tmp){using std::swap; swap(member1,tmp.member1); swap(member3,tmp.member3); };. Имеет ли это смысл? А как насчет параметра tmp, следует ли его передавать как значение? - person Javier; 15.06.2011
comment
@ Хавьер: Да, тогда имеет смысл копировать (принимая аргумент по значению) и заменять (меняя местами отдельные элементы). Ну, поскольку метод с именем copy(), который копирует только часть объекта, вообще имеет смысл. (Я бы по крайней мере назвал его copy_from() или что-то подобное, чтобы было понятнее, в каком направлении.) - person sbi; 15.06.2011
comment
@sbi, отлично! Спасибо за объяснения! Но я не совсем уверен, что понял, почему аргумент должен быть по значению? - person Javier; 15.06.2011
comment
@ Хавьер: Потому что вам все равно нужно сделать копию. (Вы не хотите менять местами исходный аргумент, а его копию.) Вы также можете пройти по ссылке const, а затем сделать копию аргумента внутри функции. Однако идиоматический способ сделать это - взять аргумент по копии. - person sbi; 15.06.2011
comment
Безопасно ли отмечать T& operator=(T tmp) как noexcept? - person becko; 21.07.2015
comment
@becko: В общем, нет. Он вызывает ctor копирования, и, как правило, они могут выделять ресурсы, что может привести к сбою и возникновению исключений. - person sbi; 22.07.2015
comment
@sbi Я думаю, что согласен с ответом Дэниела Фрея здесь: stackoverflow.com/a/18848460/855050 (который я нашел позже ). Если у вас есть встречный аргумент, поделитесь им со мной. - person becko; 22.07.2015
comment
@becko: Он действительно очень хорошо замечает, поэтому я отказываюсь от своего заявления и настаиваю на обратном. :) - person sbi; 22.07.2015

Уже есть несколько хороших ответов. Я сосредоточусь в основном на том, чего, по моему мнению, им не хватает - объяснении "минусов" с идиомой копирования и обмена ....

Что такое идиома копирования и обмена?

Способ реализации оператора присваивания в терминах функции подкачки:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

Основная идея заключается в следующем:

  • наиболее подверженная ошибкам часть присвоения объекту - обеспечение любых ресурсов, необходимых для нового состояния (например, память, дескрипторы)

  • это получение может быть предпринято перед изменением текущего состояния объекта (т. е. *this), если создается копия нового значения, поэтому rhs принимается по значению ( т.е. скопировано), а не по ссылке

  • замена состояния локальной копии rhs и *this обычно относительно легко обходится без потенциальных сбоев / исключений, учитывая, что локальной копии впоследствии не требуется какое-либо конкретное состояние (просто необходимо состояние, подходящее для деструктора, чтобы run, как и для объекта, который перемещается из> = C ++ 11)

Когда его следует использовать? (Какие проблемы он решает [/ create]?)

  • Если вы хотите, чтобы назначенный объект не был затронут назначением, которое генерирует исключение, при условии, что у вас есть или вы можете написать swap со строгой гарантией исключения, и в идеале такой, который не может завершиться ошибкой / _7 _ .. †

  • Если вам нужен чистый, простой для понимания и надежный способ определения оператора присваивания в терминах (более простого) конструктора копирования, swap и функций деструктора.

    • Self-assignment done as a copy-and-swap avoids oft-overlooked edge cases.‡

  • Когда любое снижение производительности или кратковременное увеличение использования ресурсов, созданное из-за наличия дополнительного временного объекта во время назначения, не имеет значения для вашего приложения. ⁂

swap throwing: it's generally possible to reliably swap data members that the objects track by pointer, but non-pointer data members that don't have a throw-free swap, or for which swapping has to be implemented as X tmp = lhs; lhs = rhs; rhs = tmp; and copy-construction or assignment may throw, still have the potential to fail leaving some data members swapped and others not. This potential applies even to C++03 std::string's as James comments on another answer:

@wilhelmtell: В C ++ 03 нет упоминания об исключениях, которые могут быть вызваны std :: string :: swap (который вызывается std :: swap). В C ++ 0x std :: string :: swap не является исключением и не должен вызывать исключения. - Джеймс МакНеллис 22 дек.


‡ Реализация оператора присваивания, которая кажется разумной при присваивании из отдельного объекта, может легко потерпеть неудачу из-за самостоятельного присваивания. Хотя может показаться невообразимым, что клиентский код будет даже пытаться назначить себя самому, это может произойти относительно легко во время операций алгоритма с контейнерами, с x = f(x); кодом, где f (возможно, только для некоторых #ifdef ветвей) макрос ala #define f(x) x или функция, возвращающая ссылку до x или даже (вероятно, неэффективного, но краткого) кода типа x = c1 ? x * 2 : c2 ? x / 2 : x;). Например:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

При самостоятельном назначении указанный выше код удаляет x.p_;, указывает p_ на недавно выделенную область кучи, а затем пытается прочитать в ней неинициализированные данные (неопределенное поведение), если это не делает ничего слишком странного, copy пытается присваивать себя каждому только что разрушенному "T"!


⁂ Идиома копирования и обмена может привести к неэффективности или ограничениям из-за использования дополнительного временного (когда параметр оператора создается копированием):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Здесь рукописный Client::operator= может проверять, подключен ли *this к тому же серверу, что и rhs (возможно, отправив код "сброса", если он полезен), тогда как подход копирования и обмена вызовет конструктор копирования, который, вероятно, будет написано, чтобы открыть отдельное соединение сокета, а затем закрыть исходное. Это могло не только означать удаленное сетевое взаимодействие вместо простой копии внутрипроцессной переменной, но и могло противоречить ограничениям клиента или сервера на ресурсы или соединения сокетов. (Конечно, у этого класса довольно ужасный интерфейс, но это другое дело ;-P).

person Tony Delroy    schedule 06.03.2014
comment
Тем не менее, соединение через сокет было всего лишь примером - тот же принцип применяется к любой потенциально дорогостоящей инициализации, такой как проверка / инициализация / калибровка оборудования, создание пула потоков или случайных чисел, определенные задачи криптографии, кеши, сканирование файловой системы, база данных. соединения и т. д. - person Tony Delroy; 21.10.2014
comment
Есть еще один (массовый) минус. Согласно текущим спецификациям технически объект не будет иметь оператора присваивания перемещения! Если позже будет использоваться как член класса, новый класс не будет иметь оператора перемещения-присваивания! ctor создается автоматически! Источник: youtu.be/mYrbivnruYw?t=43m14s - person user362515; 14.02.2015
comment
Основная проблема с оператором присваивания копии Client заключается в том, что присваивание не запрещено. - person sbi; 22.07.2015
comment
В примере с клиентом класс следует сделать не копируемым. - person John Z. Li; 03.05.2019

Этот ответ больше похож на дополнение и небольшую модификацию ответов выше.

В некоторых версиях Visual Studio (и, возможно, в других компиляторах) есть ошибка, которая действительно раздражает и не имеет смысла. Итак, если вы объявите / определите свою функцию swap следующим образом:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

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

введите описание изображения здесь

Это как-то связано с вызовом friend функции и передачей объекта this в качестве параметра.


Чтобы решить эту проблему, не используйте ключевое слово friend и переопределите функцию swap:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

На этот раз вы можете просто вызвать swap и передать other, порадовав компилятор:

введите описание изображения здесь


В конце концов, вам не нужно использовать функцию friend, чтобы поменять местами 2 объекта. Не менее разумно сделать swap функцию-член, которая имеет один other объект в качестве параметра.

У вас уже есть доступ к объекту this, поэтому передача его в качестве параметра технически избыточна.

person Oleksiy    schedule 04.09.2013
comment
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg. Это упрощенная версия. Кажется, что ошибка возникает каждый раз, когда функция friend вызывается с параметром *this - person Oleksiy; 04.09.2013
comment
@GManNickG, как я уже сказал, это ошибка, которая может хорошо работать для других людей. Я просто хотел помочь некоторым людям, у которых может быть такая же проблема, как и у меня. Я пробовал это как с Visual Studio 2012 Express, так и с предварительным просмотром 2013 года, и единственное, что заставило его исчезнуть, это моя модификация - person Oleksiy; 04.09.2013
comment
Да, определенно ошибка. Вероятно, это должен быть комментарий к существующему ответу, а не ответ, поскольку он не отвечает на фактический вопрос. Люди могут -1 это. - person GManNickG; 04.09.2013
comment
@GManNickG он не поместился бы в комментарии со всеми изображениями и примерами кода. И это нормально, если люди голосуют против, я уверен, что есть кто-то, у кого такая же ошибка; информация в этом посте может быть именно тем, что им нужно. - person Oleksiy; 04.09.2013
comment
обратите внимание, что это всего лишь ошибка в выделении кода IDE (IntelliSense) ... Он будет компилироваться нормально, без предупреждений / ошибок. - person Amro; 11.10.2013
comment
Сообщите об ошибке VS здесь, если вы еще этого не сделали (и если она не была исправлена) connect.microsoft. com / VisualStudio - person Matt; 13.05.2014
comment
Спустя год после этого поста эта ошибка все еще не исправлена ​​?! - person simonides; 01.12.2014
comment
Я понимаю, что мотивация для этого подхода может заключаться в том, чтобы просто обойти IDE, но вы привели разумный аргумент в пользу избыточности при определении функции friend. Почему это не подход к реализации по умолчанию? Это просто вопрос философии C ++ или случайно friend стал самым распространенным? Является ли распространенным сценарием, когда кто-то другой, кроме самого класса, будет вызывать swap? - person villasv; 12.11.2015

Я хотел бы добавить слово предупреждения, когда вы имеете дело с контейнерами, поддерживающими распределитель в стиле C ++ 11. Обмен и назначение имеют слегка разную семантику.

Для конкретности давайте рассмотрим контейнер std::vector<T, A>, где A - некоторый тип распределителя с отслеживанием состояния, и сравним следующие функции:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Назначение обеих функций 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, то нам нужна динамическая проверка.

    • If a.get_allocator() == b.get_allocator(), then the two containers use compatible storage, and swapping proceeds in the usual fashion.
    • Однако, если a.get_allocator() != b.get_allocator(), программа имеет неопределенное поведение (см. [Container.requirements.general / 8].

В результате свопинг стал нетривиальной операцией в C ++ 11, как только ваш контейнер начинает поддерживать распределители с отслеживанием состояния. Это несколько «продвинутый вариант использования», но он не совсем маловероятен, поскольку оптимизация перемещения обычно становится интересной только тогда, когда ваш класс управляет ресурсом, а память является одним из самых популярных ресурсов.

person Kerrek SB    schedule 24.06.2014