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


2000

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

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


7
gotw.ca/gotw/059.htm від Herb Sutter
DumbCoder

2
Дивовижно, я пов’язав це питання зі своєю відповіддю, щоб перенести семантику .
fredoverflow

4
Хороша ідея отримати повне пояснення цієї ідіоми, це настільки часто, що всі повинні знати про неї.
Матьє М.

16
Попередження: Ідіома копіювання / заміни використовується набагато частіше, ніж це корисно. Це часто шкідливо для продуктивності, коли від присвоєння копії не потрібна сувора гарантія безпеки виключень. І коли для призначення копії потрібна сильна безпека винятків, вона легко забезпечується короткою загальною функцією, на додаток до набагато швидшого оператора призначення копії. Див. Слайди.net/ripplelabs/howard-hinnant-accu2014 слайди 43 - 53. Короткий зміст: копія / заміна є корисним інструментом у панелі інструментів. Але вона надмірно продається і згодом часто зловживається.
Говард Хіннант

2
@HowardHinnant: Так, +1 до цього. Я писав це в той момент, коли майже на кожне питання C ++ було "допоможи моєму класу вийти з ладу при його копіюванні", і це була моя відповідь. Це доцільно, коли ви просто хочете працювати семантику копіювання / переміщення чи будь-що інше, щоб перейти до інших речей, але це не дуже оптимально. Не соромтеся поставити відмову у верхній частині моєї відповіді, якщо ви думаєте, що це допоможе.
GManNickG

Відповіді:


2183

Огляд

Навіщо нам потрібна ідіома копіювання та заміни?

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

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

Як це працює?

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

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

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

(Мало того, але некваліфіковані дзвінки 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функцію .

Нам потрібно додати функціонал 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;
}

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

І так завершується ідіомою копіювання та заміни.


Виноски

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

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

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

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


17
@GMan: Я б стверджував, що клас, що керує кількома ресурсами одночасно, приречений на збій (безпека виключень стає кошмарним), і я настійно рекомендую або класу керувати ОДНИМ ресурсом, або він має бізнес-функціональність і використовувати менеджерів.
Матьє М.

22
Я не розумію, чому метод swap тут оголошено другом?
szx

9
@asd: щоб дозволити його знайти через ADL.
GManNickG

8
@neuviemeporte: за допомогою дужок елементи масивів ініціалізуються за замовчуванням. Без цього вони неініціалізовані. Оскільки в конструкторі копій ми все одно перезапишемо значення, ми можемо пропустити ініціалізацію.
GManNickG

10
@neuviemeporte: Вам потрібно swapзнайти його під час ADL, якщо ви хочете, щоб він працював у більшості загальних кодів, на які ви натрапите, як boost::swapі інші різні випадки заміни. Swap є складним питанням на C ++, і, як правило, ми всі дійшли згоди, що найкраща єдина точка доступу (для послідовності), і єдиний спосіб зробити це взагалі - це безкоштовна функція ( intне може бути учасник swap, наприклад). Дивіться моє запитання щодо деякої інформації.
GManNickG

274

По суті, призначення - це два кроки: руйнування старого стану об'єкта та створення його нового стану як копії стану іншого об'єкта.

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

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

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

1
Я думаю, що згадка про pimpl так само важлива, як і згадка про копію, заміну та знищення. Зміна не є безпечною для винятків. Це безпечно для винятків, оскільки заміна покажчиків безпечна для винятків. Вам не доведеться використовувати pimpl, але якщо цього не зробити, ви повинні переконатися, що кожна заміна члена є безпечною для винятків. Це може бути кошмаром, коли ці члени можуть змінитися, і це банально, коли вони сховані за сутенера. А потім, то приходить вартість сутенера. Що призводить нас до висновку, що безпека виключень часто несе витрати на продуктивність.
wilhelmtell

7
std::swap(this_string, that)не дає гарантії без кидок. Це забезпечує високу безпеку виключень, але не є гарантією без кидок.
wilhelmtell

11
@wilhelmtell: У С ++ 03 не згадується про винятки, які потенційно може бути викинуто std::string::swap(що викликається std::swap). У C ++ 0x std::string::swapє noexceptі не повинен кидати винятків.
Джеймс Мак-Нілліс

2
@sbi @JamesMcNellis нормально, але справа все-таки стоїть: якщо у вас є члени класового типу, ви повинні переконатися, що заміна ними не є кидкою. Якщо у вас є один член, який є вказівником, то це банально. Інакше це не так.
wilhelmtell

2
@wilhelmtell: Я думав, що це був пункт обміну: він ніколи не кидає, і це завжди O (1) (так, я знаю, std::array...)
sbi

44

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

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

Спосіб реалізації оператора присвоєння з точки зору функції swap:

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

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

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

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

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

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

  • Якщо ви хочете, щоб на об'єкт, на який покладено заперечення, не впливав завдання, яке викидає виняток, якщо припустити, що ви маєте або можете написати 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 грудня 1010 о 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намагається самостійно призначити кожен справедливий- зруйнований 'T'!


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

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
Однак, приклад з'єднання - лише приклад - той самий принцип застосовується до будь-якої потенційно дорогої ініціалізації, наприклад, апаратне зондування / ініціалізація / калібрування, генерування пулу потоків або випадкових чисел, певні завдання криптографії, кеші, сканування файлової системи, база даних підключення тощо.
Тоні Делрой

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

3
Основна проблема оператора присвоєння копії в Clientтому, що присвоєння не заборонено.
sbi

У прикладі клієнта клас повинен бути зроблений без копіювання.
Джон З. Лі

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функцію для обміну двома об’єктами. Це має стільки ж сенсу, щоб зробити 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, так і з попереднім переглядом 2013 року, і єдине, що змусило її піти - це моя модифікація
Олексій

8
@GManNickG він не впишеться в коментар до всіх зображень та прикладів коду. І це нормально, якщо люди звертаються з головою, я впевнений, що хтось там отримує таку ж помилку; інформація в цій публікації може бути саме тим, що їм потрібно.
Олексій

14
зауважте, що це лише помилка в підсвічуванні коду IDE (IntelliSense) ... Вона складеться чудово, без попереджень / помилок.
Amro

3
Повідомте про помилку VS, якщо ви цього ще не зробили (і якщо вона не була виправлена) connect.microsoft.com/VisualStudio
Matt

15

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.