Питання Що таке ідіома копіювання та обміну?


Що це таке ідіома і коли його слід використовувати? Які проблеми це вирішує? Чи змінюється ідіома, коли використовується C ++ 11?

Хоча це було згадано у багатьох місцях, ми не мали жодного сингулярного "що це" питання і відповіді, отже, це так. Ось неповний список місць, де це було згадано раніше:


1671
2017-07-19 08:42


походження


gotw.ca/gotw/059.htm від Герба Саттера - DumbCoder
Чудово, я пов'язав це питання з моїм відповідь на переміщення семантики. - fredoverflow
Гарна ідея мати повне пояснення цієї ідіоми, це так часто, що всі повинні знати про це. - Matthieu M.
Попередження: ідіотом копіювання / заміни використовується набагато частіше, ніж корисно. Це часто є шкідливим для продуктивності, коли не вимагається надійна гарантія захисту від призначення копії. І коли для призначення синтаксису потрібна сильна виняткова безпека, її можна легко забезпечити короткою загальною функцією, крім набагато швидшого оператора присвоєння копії. Побачити slideshare.net/ripplelabs/howard-hinnant-accu2014 слайди 43-53. Резюме: копіювання / обмін є корисним інструментом на панелі інструментів. Але це було надмірно продано, і згодом його часто зловживали. - Howard Hinnant
@HowardHinnant: Так, +1 до цього. Я написав це в той час, коли практично кожне запитання C ++ було "допомогти моєму класу аварійно завершити роботу при його копіюванні", і це була моя відповідь. Це правильно, коли ви просто хочете працювати copy-/ move-семантика або що-небудь, щоб ви могли перейти до інших речей, але це не є оптимальним. Не соромтеся поставити заперечення у верхній частині моєї відповіді, якщо ви вважаєте, що це допоможе. - GManNickG


Відповіді:


Огляд

Чому нам потрібен ідіом копіювання та обміну?

Будь-який клас, який керує ресурсом (a обгортка, як смарт-покажчик) потрібно реалізувати Велика трійка. Хоча цілі та реалізація копіювання-конструктора та деструктора є простими, оператор присвоєння копії є, мабуть, найбільш нюансовим і складним. Як це зробити? Які пастки потрібно уникати?

The копіювання та перестановка ідіома це рішення, і елегантно допомагає оператору присвоювання досягти двох речей: уникати дублювання коду, і надання сильна гарантія винятку.

Як це працює?

Концептуально, він працює за допомогою функції копіювання конструктора для створення локальної копії даних, а потім приймає скопійовані дані з swap функція, обмінюючи старі дані новими даними. Тимчасова копія потім руйнує, беручи із нею старі дані. Нам залишається копія нових даних.

Для того, щоб скористатися ідіомою copy-swap, нам потрібні три речі: робочий копіювальний конструктор, робочий деструктор (обидва є основою будь-якої обгортки, тому вони повинні бути повними в будь-якому випадку), а також swap функція

Функція підкачки є a не кидаючи функція, яка обмінюється двома об'єктами класу, членом для члена. Можливо, ми будемо спокушатися std::swap замість того, щоб забезпечити себе, але це було б неможливо; std::swap використовує оператор copy-constructor і copy-assignment в рамках його реалізації, і в кінцевому підсумку ми намагаємося визначити оператора призначення в самій собі!

(Не тільки це, але і некваліфіковані дзвінки 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 це положення не є проблемою. Це тому, що клас повинен керувати лише один ресурс!)

Успішне рішення

Як уже згадувалося, ідіома копіювання та обміну вирішить всі ці проблеми. Але зараз ми маємо всі вимоги, крім одного: a swap функція Хоча Правило трьох успішно спричиняє існування нашого копіювального конструктора, оператора призначення та деструктора, його слід насправді назвати "Великою трійкою та половиною": у будь-який час, коли ваш клас керує ресурсом, має сенс забезпечити 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'S, але взагалі обміни можуть бути більш ефективними; він просто обмінює покажчики та розміри, а не виділяє і копіює цілі масиви. Окрім цього бонусу у функціональності та ефективності, ми зараз готові реалізувати ідіому копіювання та обміну.

Без додаткової реклами наш оператор присвоювання:

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, який обговорюється пізніше. (На загальній записці надзвичайно корисне керівництво полягає в наступному: якщо ви збираєтеся зробити копію щось у функції, дайте компілятору зробити це у списку параметрів ‡)

Так чи інакше, цей спосіб отримання нашого ресурсу є ключем до усунення копіювання коду: ми можемо використати код з copy-constructor для створення копії, і ніколи не потрібно повторити його. Тепер, коли копія виконана, ми готові обміняти.

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

На цьому етапі ми бездоганні, тому що swap не кидається Ми замінюємо наші поточні дані скопійованими даними, благополучно змінюємо нашу державу, а старі дані стають тимчасовими. Старі дані потім випускаються, коли функція повертається. (Де закінчується область дії параметра і викликається його деструктор).

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

І це копіювання та обмін ідіом.

А як щодо C + + 11?

Наступна версія C ++, C ++ 11, робить одну дуже важливу зміну в тому, як ми керуємо ресурсами: зараз Правило трьох Правило четверо (і півтора). Чому? Оскільки нам потрібно не тільки копіювати-конструювати наш ресурс, ми повинні рухатись - конструювати це також.

На щастя, для нас це легко:

class dumb_array
{
public:
    // ...

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

    // ...
};

Що тут відбувається? Нагадаємо, мета переміщення-побудова: взяти ресурси з іншої екземпляри класу, залишивши її в державі, що гарантовано буде призначена і руйнується.

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

(Зверніть увагу, що деякі компілятори не підтримують делегування конструктора, в цьому випадку нам доведеться вручну задати конструкцію класу. Це невдале, але, на щастя, тривіальне завдання.)

Чому це працює?

Це єдина зміна, яку нам потрібно зробити у нашому класі, то чому це працює? Пам'ятайте про все важливе рішення, яке ми зробили, щоб зробити параметр значенням, а не посиланням:

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

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

І так закінчується ідіом копіювання та обміну.


Виноски

* Чому ми встановили mArray нуль Тому що, якщо будь-який додатковий код в операторі кидає, деструктор dumb_array можна назвати; і якщо це станеться без встановлення значення null, ми спробуємо видалити вже видалену пам'ять! Ми уникаємо цього, встановивши його на нуль, оскільки видалення null - бездіяльність.

† Є інші претензії, які ми повинні спеціалізуватися std::swap для нашого типу надайте в класі swap уздовж вільної функції swapі т. д. Але це все непотрібно: будь-яке належне використання swap буде через некваліфікований дзвінок, і наша функція буде знайдена через ADL. Одна функція буде робити.

‡ Причина проста: коли ви маєте ресурс для себе, ви можете обміняти та / або перемістити його (C ++ 11) де завгодно. І, зробивши копію у списку параметрів, ви максимізуєте оптимізацію.


1841
2017-07-19 08:43



@GMan: Я б стверджував, що клас, керуючий кількома ресурсами відразу, приречений на провал (виняток безпеки стає жахливим), і я б настійно рекомендував, щоб будь-який клас керував ОДним ресурсом, він має бізнес-функції та використовує менеджери. - Matthieu M.
@FrEEzE: "Це специфічний компілятор, в якому порядку список обробляється". Ні це не так. Він обробляється в тому порядку, в якому вони відображаються у визначенні класу. Компілятор, який не приймає std::copy цей шлях зламаний, я не кодую розбитих компіляторів. І я не впевнений, що розумію ваш останній коментар. - GManNickG
@Freeze: Крім того, точка цієї відповіді полягає в тому, щоб говорити про ідіом C ++. Якщо вам потрібно зламати вашу програму для роботи з несумісним компілятором, це добре, але не намагайтеся діяти так, як це відповідає моїй відповідальності, або що це - "погана практика", щоб не, будь ласка. - GManNickG
Я не розумію, чому метод swap тут оголошений як друг? - szx
@neuviemeporte: Вам потрібна ваша swap щоб бути знайдений під час роботи ADL, якщо ви хочете, щоб він працював у більшості загальних кодів, які ви зустрінете, наприклад boost::swap та інші різні випадки обміну. Обмін є складною проблемою в C ++, і, як правило, всі ми прийшли до згоди про те, що найкраще є одна точка доступу (для послідовності), і єдиним способом зробити це взагалі є вільна функція (int не може мати, наприклад, учасника підкачки). Побачити моє питання для певного фону. - GManNickG


Призначення в його серці складається з двох етапів: зриваючи стару державу об'єкта і будуючи свій новий стан як копію стану якогось іншого об'єкта.

В принципі, це те, що деструктор і скопіювати конструктор так, перша ідея - делегувати їм роботу. Однак, оскільки знищення не повинно зазнати невдачі, тоді як будівництво може, ми насправді хочемо зробити це навпаки: спочатку виконують конструктивну частину і якщо це вдалося потім роблять руйнівну частину. Ідея копіювання та обміну є способом зробити саме це: він спочатку викликає конструктор копій класу, щоб створити тимчасове, а потім обмінювати своїми даними з тимчасовими, а потім дозволить тимчасовому деструктору руйнувати старе стан.
З тих пір swap() передбачається, що ніколи не вдасться, єдина частина, яка може вийти з ладу, - це побудова копіювання. Це виконується спочатку, і якщо воно не відбудеться, ніщо не буде змінено в цільовому об'єкті.

У витонченій формі копіювання та обмін здійснюється за наявності копії шляхом ініціалізації параметра (невідповідності) оператора присвоювання:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

227
2017-07-19 08:55



Я думаю, що згадка про pimpl так само важлива, як згадування копії, обмін і руйнування. Обмін не є магічною винятковою безпекою. Це винятково безпечно, оскільки покажчики для обміну є виключно безпечними. Ти не маєш мати використовувати pimpl, але якщо ви цього не зробите, ви повинні переконатися, що кожен обмін членом є виключно безпечним. Це може бути кошмар, коли ці члени можуть змінюватися, і це тривіальне, коли вони ховаються за пімпл. І тоді, потім приходить вартість пімпл. Це приводить нас до висновку, що часто виняток-безпека несе в собі витрати на продуктивність. - wilhelmtell
std::swap(this_string, that) не забезпечує гарантію без бронювання. Це забезпечує сильний винятковий рівень безпеки, але не гарантія без кидання. - wilhelmtell
@wilhelmtell: У C ++ 03 немає згадки про винятки, які потенційно кинуті std::string::swap (який називається std::swap) У C ++ 0x std::string::swap є noexcept і не повинні викидати винятки. - James McNellis
@sbi @JamesMcNellis добре, але ця точка все ще стоїть: якщо у вас є члени класового типу, ви повинні переконатися, що їх обмін - це без кидка. Якщо у вас є один член, який є покажчиком, то це тривіальне значення. Інакше це не так. - wilhelmtell
@wilhelmtell: Я подумав, що це був пункт обміну: він ніколи не кидає, і це завжди O (1) (так, я знаю, std::array...) - sbi


Є вже хороші відповіді. Я зосередитимусь головним чином на що я думаю їм не вистачає - пояснення "мінусів" з копією-swap idiom ....

Що таке ідіома копіювання та обміну?

Спосіб здійснення оператора присвоювання за допомогою функції своп:

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

Основна ідея полягає в тому, що:

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

  • це придбання можна спробувати раніше модифікація поточного стану об'єкта (тобто *this), якщо зроблено копію нового значення, тож і тому rhs приймається за вартістю (тобто скопійований), а не за посиланням

  • обмін станом локальної копії rhs і *this є зазвичай відносно легко обійтися без можливих збоїв / винятків, оскільки локальна копія не потребує жодного конкретного стану пізніше (просто потрібно стан, придатний для роботи деструктора, так само як і для об'єкта переїхав з в> = C ++ 11)

Коли його слід використовувати? (Які проблеми це вирішує [/ create]?)

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

  • Якщо ви хочете чистого, зрозумілого, надійного способу визначити оператора призначення за допомогою (простішого) конструктора копії, swap і деструктор функції.

    • Самостійне призначення, виконане як копіювання та обмін, дозволяє уникати випадків, коли ви не помічаєте краю. ‡

  • Якщо будь-який стягнення за виконання або тимчасовий вищий ресурс, створений за рахунок додаткового тимчасового об'єкта під час призначення, не важливо для вашої програми. ⁂

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


‡ реалізація оператора присвоєння, яка виглядає здоровою, коли присвоєння з певного об'єкта може легко зазнати невдачі для самостійного призначення. Хоча може здатися неймовірним те, що клієнтський код навіть намагається самостійно призначати себе, це може статися порівняно легко під час операцій на контейнері, коли 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_ в новому розподіленому регіоні купу, потім намагається прочитати неініціалізований дані в ній (невизначена поведінка), якщо це не робить нічого дивного, copy намагається самостійно розпоряджатись кожному просто розбитому "Т"!


⁂ Ідиом копіювання та заміни може вводити неефективність або обмеження внаслідок використання додаткового тимчасового (коли параметр оператора побудований копією):

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


32
2018-03-06 14:51



Тим не менш, приєднання сокетів було лише прикладом - такий же принцип застосовується до будь-якої потенційно дорогої ініціалізації, такої як апаратне зондування / ініціалізація / калібрування, створення пулу потоків чи випадкових чисел, певні задачі криптографії, кеш-пам'ять, сканування файлової системи, база даних з'єднання тощо. - Tony Delroy
Є ще один (масивний) кон. З поточних специфікацій технічно об'єкт буде не має оператора переміщення-призначення! Якщо пізніше використовуватися як член класу, то новий клас не буде автоматично створено переміщення-ctor! Джерело: youtu.be/mYrbivnruYw? t = 43m14s - user362515
Основна проблема з оператором присвоєння копії Client це призначення не заборонено. - sbi


Ця відповідь більше схожа на додавання та невелику зміну відповідей вище.

У деяких версіях Visual Studio (і, можливо, інших компіляторів) є помилка, яка дійсно дратує і не має сенсу. Отже, якщо ви оголошувати / визначити своє swap функціонує так:

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

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

}

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

enter image description here

Це має дещо пов'язане з a friend функція називається і this об'єкт переданий як параметр.


Шляхом цього не використовувати friend ключове слово та перевизначити swap функція:

void swap(A& other) {

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

}

Цього разу ви можете просто зателефонувати swap і пройти other, що робить компілятор щасливим:

enter image description here


Зрештою, у вас немає треба використовувати а friend функція для обміну 2 об'єктів. Це робить стільки ж сенсу swap функція члена, яка має таку other об'єкт як параметр.

Ви вже маєте доступ до this об'єкт, тому передавання його в якості параметра технічно є зайвим.


19
2017-09-04 04:50



Чи можете ви поділитися своїм прикладом, який відтворює помилку? - GManNickG
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp  dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg. Це спрощена версія. Помилка виникає щоразу, коли a friend функція викликається з *this параметр - Oleksiy
@GManNickG він не вписувався в коментар із зображеннями та прикладами коду. І все гаразд, якщо люди знизяться, я впевнений, що є хтось там, хто отримує таку саму помилку; інформація на цій посаді може бути саме те, що їм потрібно. - Oleksiy
Зауважте, що це лише помилка підсвічування коду IDE (IntelliSense) ... Він буде компілюватися просто без будь-яких попереджень / помилок. - Amro
Будь-ласка, повідомте про помилку VS, якщо ви ще цього не зробили (і якщо вона не була виправлена) connect.microsoft.com/VisualStudio - Matt


Я хотів би додати слово попередження, коли ви маєте справу з контейнерами, присвяченими 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, як тільки ваш контейнер починає підтримувати старі розподілювачі. Це дещо "розширений варіант використання", але це не зовсім малоймовірно, оскільки оптимізація переміщення зазвичай стає цікавою тільки тоді, коли ваш клас керує ресурсом, а пам'ять є одним з найпопулярніших ресурсів.


10
2018-06-24 08:16