Як працює гарантоване копіювання?


89

На засіданні стандартів ISO O ++ в Оулу у 2016 році комісія зі стандартів проголосувала пропозицію під назвою Гарантоване копіювання шляхом спрощених категорій цінності .

Як саме працює гарантоване копіювання? Чи охоплює це деякі випадки, коли копіювання elision вже було дозволено, чи потрібні зміни коду, щоб гарантувати копіювання elision?

Відповіді:


129

Копіювання елізії було дозволено здійснювати за ряду обставин. Однак, навіть якщо це було дозволено, код все одно повинен мати можливість працювати так, ніби копія не була видалена. А саме, повинен бути доступний конструктор копії та / або переміщення.

Гарантований копія елізія переопределяет ряд понять , C ++, наприклад , що деякі обставини , при яких копій / кроки можуть бути опущені на насправді не спровокувати копіювання / переміщення на всіх . Компілятор не видаляє копію; стандарт говорить, що такого копіювання ніколи не могло статися.

Розглянемо цю функцію:

T Func() {return T();}

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

Аналогічно:

T t = Func();

Це ініціалізація копії t. Буде скопійовано ініціалізацію tзі значенням, що повертається Func. Однак, Tяк і раніше, повинен бути конструктор переміщення, хоча він і не буде викликаний.

Гарантована копія елізії перевизначає значення виразу першого значення . Pre-C ++ 17, prvalues ​​- це тимчасові об'єкти. У C ++ 17 вираз першого значення - це лише те, що може матеріалізуватися тимчасовим, але це ще не тимчасове.

Якщо ви використовуєте prvalue для ініціалізації об'єкта типу prvalue, тоді жодне тимчасове не матеріалізується. Коли ви це зробите return T();, це ініціалізує повернене значення функції за допомогою першого значення. Оскільки ця функція повертається T, тимчасові не створюються; ініціалізація першого значення просто безпосередньо ініціює повернене значення.

Потрібно зрозуміти, що оскільки повернене значення є першим значенням, воно ще не є об’єктом . Це просто ініціалізатор об’єкта, як і T()є.

Коли ви це зробите T t = Func();, перше значення поверненого значення безпосередньо ініціалізує об'єкт t; немає етапу "створити тимчасовий і скопіювати / перемістити". Оскільки Func()повертається значення '' - це перше значення, еквівалентне T(), tбезпосередньо ініціалізується T(), точно так само, як якщо б ви це зробили T t = T().

Якщо перше значення використовується будь-яким іншим способом, перше значення буде матеріалізувати тимчасовий об'єкт, який буде використаний у цьому виразі (або відкинутий, якщо відсутній вираз). Отже, якщо б ви це зробили const T &rt = Func();, prvalue матеріалізував би тимчасовий (з використанням T()як ініціалізатор), посилання якого буде зберігатися rtразом із звичайними тимчасовими матеріалами продовження життя.

Гарантована елізія дозволяє зробити одне - повернути об’єкти, що не рухаються. Наприклад, lock_guardне можна скопіювати або перемістити, тому у вас не може бути функції, яка повертає її за значенням. Але з гарантованим копіюванням ви можете.

Гарантована елісія також працює з прямою ініціалізацією:

new T(FactoryFunction());

Якщо FactoryFunctionповертається Tза значенням, цей вираз не копіює значення, що повертається, у виділену пам'ять. Натомість він виділить пам’ять і використає виділену пам’ять як пам’ять із зворотним значенням для безпосереднього виклику функції.

Тож заводські функції, які повертаються за значенням, можуть безпосередньо ініціалізувати купу виділеної пам'яті, навіть не знаючи про це. Поки ці функції внутрішньо відповідають правилам гарантованого копіювання, звичайно. Вони повинні повернути перше значення типу T.

Звичайно, це теж працює:

new auto(FactoryFunction());

Якщо вам не подобається писати імена типів.


Важливо визнати, що вищезазначені гарантії працюють лише для цінностей. Тобто ви не отримуєте жодних гарантій при поверненні іменованої змінної:

T Func()
{
   T t = ...;
   ...
   return t;
}

У цьому випадку tвсе одно повинен бути доступний конструктор копіювання / переміщення. Так, компілятор може вибрати оптимізацію копіювання / переміщення. Але компілятор все одно повинен перевірити наявність доступного конструктора копіювання / переміщення.

Отже, нічого не змінюється для оптимізованого іменованого значення (NRVO).


1
@BenVoigt: Внесення нетривіально копіюваних визначених користувачем типів до реєстрів - це не життєздатна річ, яку ABI може зробити, незалежно від того, доступна elision чи ні.
Нікол Болас

1
Тепер, коли правила є загальнодоступними, можливо, варто оновити їх за допомогою концепції "prvalues ​​- це ініціалізації".
Йоханнес Шауб - літб

6
@ JohannesSchaub-litb: Це "неоднозначно" лише в тому випадку, якщо ви знаєте занадто багато про деталі стандарту C ++. Для 99% спільноти C ++ ми знаємо, до чого відноситься "гарантоване копіювання елізії". Фактична стаття, що пропонує цю функцію, навіть має назву "Гарантоване копіювання". Додавання "через спрощені категорії цінностей" просто ускладнює та ускладнює розуміння користувачів. Крім того, це неправильна назва, оскільки ці правила насправді не "спрощують" правила навколо категорій цінностей. Хочете ви цього чи ні, але термін "гарантоване копіювання" стосується цієї функції і нічого іншого.
Нікол Болас,

1
Я так хочу мати можливість підібрати перше значення та носити його з собою. Я думаю, що це просто (один постріл) std::function<T()>насправді.
Якк - Адам Неврамонт,

1
@LukasSalich: Це питання на C ++ 11. Ця відповідь стосується функції C ++ 17.
Nicol
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.