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

На собрании стандартов Oulu ISO С++ 2016 года предложение, называемое Гарантированное копирование текста через упрощенные категории значений, было проголосовано на С++ 17 комитета по стандартизации.

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

Ответ 1

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

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

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

T Func() {return T();}

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

Аналогично:

T t = Func();

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

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

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

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

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

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

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

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

new T(FactoryFunction());

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

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

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

new auto(FactoryFunction());

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


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

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

В этом случае T должен по-прежнему иметь доступный конструктор copy/move. Да, компилятор может выбрать оптимизацию копирования/перемещения. Но компилятор все равно должен проверить наличие доступного конструктора copy/move.

Таким образом, ничто не изменится для именованной оптимизации возвращаемого значения (NRVO).