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

2003

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

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

GManNickG
источник
7
gotw.ca/gotw/059.htm от Херба Саттера
DumbCoder
2
Круто, я связал этот вопрос из своего ответа, чтобы переместить семантику .
fredoverflow
4
Хорошая идея иметь полное объяснение этой идиомы, это настолько часто, что каждый должен знать об этом.
Матье М.
16
Предупреждение: идиома копирования / обмена используется гораздо чаще, чем она полезна. Часто это вредно для производительности, когда нет необходимости в строгой гарантии безопасности при копировании. И когда для назначения копирования требуется строгая безопасность исключений, она легко обеспечивается короткой универсальной функцией в дополнение к гораздо более быстрому оператору назначения копирования. См. Слайды 43 - 53 на slideshare.net/ripplelabs/howard-hinnant-accu2014. Резюме: копирование / замена - полезный инструмент в наборе инструментов. Но он был перепродан и впоследствии часто подвергался насилию.
Говард Хиннант
2
@HowardHinnant: Да, +1 к этому. Я написал это в то время, когда почти каждый вопрос C ++ был «помогите моему классу разбиться при копировании», и это был мой ответ. Это уместно, когда вам просто нужна рабочая семантика копирования / перемещения или чего-то еще, чтобы вы могли перейти к другим вещам, но это не совсем оптимально. Не стесняйтесь ставить отказ от ответственности в верхней части моего ответа, если вы думаете, что это поможет.
GManNickG

Ответы:

2184

обзор

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

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

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

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

Концептуально , он работает с использованием функциональности конструктора копирования для создания локальной копии данных, а затем берет скопированные данные с помощью 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. Код расширился! Что приводит нас к третьей проблеме: дублирование кода. Наш оператор присваивания эффективно дублирует весь код, который мы уже написали в другом месте, и это ужасно.

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

(Можно задаться вопросом: если для правильного управления одним ресурсом требуется такой большой код, что если мой класс управляет более чем одним? Хотя это может показаться обоснованным, и на самом деле для этого требуются нетривиальные try/ catchпредложения, это не Это потому, что класс должен управлять только одним ресурсом !)

Успешное решение

Как уже упоминалось, идиома копирования и обмена исправит все эти проблемы. Но сейчас у нас есть все требования, кроме одного: 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);
    }

    // ...
};

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

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

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

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

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

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

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

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


Сноски

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

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

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

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

GManNickG
источник
17
@GMan: Я бы сказал, что класс, управляющий несколькими ресурсами одновременно, обречен на провал (безопасность исключений становится кошмарным), и я настоятельно рекомендую, чтобы либо класс управлял ОДНЫМ ресурсом, либо имел бизнес-функциональность и управляющие использованием.
Матье М.
22
Я не понимаю, почему метод swap объявлен здесь другом?
Szx
9
@asd: чтобы можно было найти его через ADL.
GManNickG
8
@neuviemeporte: с круглыми скобками элементы массива инициализируются по умолчанию. Без них они неинициализированы. Поскольку в конструкторе копирования мы все равно будем перезаписывать значения, мы можем пропустить инициализацию.
GManNickG
10
@neuviemeporte: Вам нужно, swapчтобы ваш файл находился во время ADL, если вы хотите, чтобы он работал в наиболее общем коде, с которым вы столкнетесь, например, boost::swapи в других различных экземплярах подкачки. Своп - сложная проблема в C ++, и, как правило, мы все согласны с тем, что лучше всего использовать одну точку доступа (для согласованности), и единственный способ сделать это в общем случае - свободная функция ( intне может иметь члена подкачки, например). Смотрите мой вопрос для некоторого фона.
GManNickG
274

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

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

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

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
SBI
источник
1
Я думаю, что упоминание прыща так же важно, как упоминание копии, обмена и уничтожения. Обмен не магически исключительный исключительный. Он безопасен для исключений, потому что замена указателей безопасна для исключений. Вам не нужно использовать pimpl, но если вы этого не сделаете, вы должны убедиться, что каждый обмен члена безопасен от исключений. Это может быть кошмар, когда эти члены могут измениться, и это тривиально, когда они прячутся за прыщами. И тогда, тогда прибывает стоимость прыща. Что приводит нас к выводу, что зачастую исключительная безопасность несет затраты в производительности.
wilhelmtell
7
std::swap(this_string, that)не дает гарантии без броска. Это обеспечивает надежную исключительную безопасность, но не гарантирует отсутствие бросков.
wilhelmtell
11
@wilhelmtell: В C ++ 03 нет упоминаний об исключениях, std::string::swapкоторые могут быть вызваны (которые вызываются std::swap). В C ++ 0x std::string::swapесть noexceptи не должно вызывать исключения.
Джеймс МакНеллис
2
@sbi @JamesMcNellis все в порядке, но все еще остается в силе: если у вас есть члены класса, вы должны убедиться, что их замена не является броском. Если у вас есть один элемент, который является указателем, это тривиально. В противном случае это не так.
wilhelmtell
2
@wilhelmtell: я думал, что это было точкой обмена: это никогда не бросает, и это всегда O (1) (да, я знаю, std::array...)
sbi
44

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

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

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

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

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

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

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

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

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

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

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

    • Самоназначение, выполняемое как копирование и обмен, позволяет избежать часто пропускаемых крайних случаев. ‡

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

swapthrowing: как правило, можно надежно поменять элементы данных, которые объекты отслеживают по указателю, но элементы без указателя данных, которые не имеют swap без бросков или для которых обмен должен быть реализован как X tmp = lhs; lhs = rhs; rhs = tmp;и конструкция копирования или присваивание может бросить, все еще может потерпеть неудачу, оставляя некоторые элементы данных замененными, а другие нет. Этот потенциал применим даже к C ++ 03 std::string, поскольку Джеймс комментирует другой ответ:

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


‡ Реализация оператора присваивания, которая кажется разумной при назначении из отдельного объекта, может легко потерпеть неудачу для самостоятельного назначения. Хотя может показаться невообразимым, что клиентский код даже попытается выполнить самостоятельное назначение, это может сравнительно легко произойти во время операций algo над контейнерами с x = f(x);кодом, в котором f(возможно, только для некоторых #ifdefветвей) есть макрос #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_на вновь выделенную область кучи, затем пытается прочитать неинициализированные в ней данные (Undefined Behavior), если это не делает ничего странного, copyпытается выполнить самостоятельное назначение каждому просто уничтожено "Т"!


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

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).

Тони Делрой
источник
4
Тем не менее, сокетное соединение было только примером - тот же принцип применим к любой потенциально дорогой инициализации, такой как аппаратное зондирование / инициализация / калибровка, генерация пула потоков или случайных чисел, определенных задач криптографии, кэшей, проверок файловой системы, базы данных соединения и т.д ..
Тони Делрой
Есть еще один (массивный) недостаток. Что касается текущих спецификаций, технически объект не будет иметь оператора присваивания! Если позже использовать его как член класса, новый класс не будет автоматически сгенерирован как Move-Ctor! Источник: youtu.be/mYrbivnruYw?t=43m14s
user362515
3
Основная проблема с оператором копирования присваивания Clientсостоит в том, что присвоение не запрещено.
СБИ
В примере клиента класс должен быть сделан некопируемым.
Джон З. Ли
25

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

В некоторых версиях 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объекту, поэтому передача его в качестве параметра технически избыточна.

Алексей
источник
1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . Это упрощенная версия. Кажется, что ошибка возникает каждый раз, когда friendфункция вызывается с *thisпараметром
Алексей
1
@GManNickG, как я уже сказал, это ошибка и может хорошо работать для других людей. Я просто хотел помочь некоторым людям, которые могут иметь ту же проблему, что и я. Я попробовал это и с Visual Studio 2012 Express, и с Preview 2013, и единственное, что заставило его уйти, была моя модификация
Алексей
8
@GManNickG это не будет соответствовать комментарию со всеми изображениями и примерами кода. И это нормально, если люди понизят голос, я уверен, что есть кто-то, кто получает такую ​​же ошибку; информация в этом посте может быть именно то, что им нужно.
Алексей
14
обратите внимание, что это всего лишь ошибка в выделении кода IDE (IntelliSense) ... Он будет прекрасно компилироваться без предупреждений / ошибок.
Amro
3
Пожалуйста, сообщите об ошибке VS здесь, если вы еще этого не сделали (и если она не была исправлена) connect.microsoft.com/VisualStudio
Matt
15

Я хотел бы добавить слово предупреждения, когда вы имеете дело с контейнерами, поддерживающими распределитель в стиле 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, то нам нужна динамическая проверка.

    • Если a.get_allocator() == b.get_allocator(), тогда два контейнера используют совместимое хранилище, и замена происходит обычным образом.
    • Однако, если a.get_allocator() != b.get_allocator()программа имеет неопределенное поведение (см. [Container.requirements.general / 8].

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

Керрек С.Б.
источник