Как работает гарантированное копирование?

89

На собрании стандартов ISO C ++ в Оулу в 2016 году комитет по стандартам проголосовал за предложение под названием « Гарантированное исключение копий с помощью упрощенных категорий значений» .

Как именно работает гарантированное копирование? Охватывает ли он некоторые случаи, когда исключение копирования уже было разрешено, или необходимы ли изменения кода, чтобы гарантировать исключение копирования?

Jotik
источник

Ответы:

129

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

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

Рассмотрим эту функцию:

T Func() {return T();}

Согласно правилам негарантированной копии, это создаст временное, а затем перейдет из этого временного в возвращаемое значение функции. Эта операция перемещения может быть опущены, но Tвсе равно должны иметь доступный конструктор двигаться , даже если он никогда не используется.

По аналогии:

T t = Func();

Это инициализация копии t. Это скопирует инициализацию tс возвращаемым значением Func. Однако Tконструктор перемещения по-прежнему должен быть, даже если он не будет вызываться.

Гарантированное исключение копирования переопределяет значение выражения prvalue . До C ++ 17 prvalue - временные объекты. В C ++ 17 выражение prvalue - это просто то, что может материализовать временное, но еще не временное.

Если вы используете prvalue для инициализации объекта типа prvalue, временное значение не материализуется. Когда вы это сделаете return T();, это инициализирует возвращаемое значение функции через prvalue. Поскольку эта функция возвращается T, временное не создается; инициализация prvalue просто напрямую инициирует возвращаемое значение.

Следует понимать, что, поскольку возвращаемое значение - это prvalue, это еще не объект . Это просто инициализатор для объекта, как и T()есть.

Когда вы это сделаете T t = Func();, prvalue возвращаемого значения непосредственно инициализирует объект t; отсутствует этап «создание временного и копирование / перемещение». Поскольку Func()возвращаемое значение является prvalue, эквивалентным T(), tинициализируется напрямую T(), точно так же, как если бы вы это сделали T t = T().

Если prvalue используется каким-либо другим способом, prvalue материализует временный объект, который будет использоваться в этом выражении (или отброшен, если выражение отсутствует). Так что, если бы вы это сделали const T &rt = Func();, prvalue материализовал бы временное значение (с использованием T()в качестве инициализатора), в котором будет храниться ссылка rtвместе с обычным временным материалом для продления срока жизни.

Одна вещь, которую позволяет вам делать гарантированная элизия, - это возвращать неподвижные объекты. Например, lock_guardего нельзя скопировать или переместить, поэтому у вас не может быть функции, возвращающей его по значению. Но с гарантированной копией вы можете.

Гарантированное исключение также работает с прямой инициализацией:

new T(FactoryFunction());

Если FactoryFunctionвозвращается Tпо значению, это выражение не копирует возвращаемое значение в выделенную память. Вместо этого он будет выделять память и использовать выделенную память как память для возвращаемых значений напрямую для вызова функции.

Таким образом, фабричные функции, которые возвращаются по значению, могут напрямую инициализировать память, выделенную кучей, даже не зная об этом. Конечно, пока эти функции внутренне следуют правилам гарантированного исключения копий. Они должны возвращать prvalue типа T.

Конечно, это тоже работает:

new auto(FactoryFunction());

Если вам не нравится писать имена типов.


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

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.
Никол Болас