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

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

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


person jotik    schedule 26.06.2016    source источник


Ответы (2)


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

Гарантированное исключение копирования переопределяет ряд концепций 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 по значению, это выражение не копирует возвращаемое значение в выделенную память. Вместо этого он будет выделять память и использовать выделенную память в качестве памяти возвращаемых значений для прямого вызова функции.

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

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

new auto(FactoryFunction());

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


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

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

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

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

person Nicol Bolas    schedule 26.06.2016
comment
Неужели не использовался ABI, который возвращал бы UDT размером со слово в регистре? Такое правило, похоже, снижает производительность итераторов и типов-оболочек, которые можно найти в библиотеках размерной корректности (у std::chrono есть некоторые из этих типов). Или, может быть, возврат в регистр все еще в порядке, если тип тривиально копируемый, так что определение того, произошло ли исключение, невозможно? - person Ben Voigt; 27.06.2016
comment
@BenVoigt: Помещение нетривиально копируемых пользовательских типов в регистры не является жизнеспособной вещью, которую может сделать ABI, независимо от того, доступна ли elision или нет. - person Nicol Bolas; 27.06.2016
comment
Теперь, когда правила являются общедоступными, возможно, стоит обновить их с помощью концепции prvalues ​​are initializations. - person Johannes Schaub - litb; 12.09.2016
comment
@ M.M: Я думаю, вы напутали свой пример. Вы, вероятно, хотели, чтобы main позвонил b, а не a. Но ответ в любом случае один и тот же: он не материализует никаких временных. вы говорите, что A () материализуется в возвращаемом объекте a () Нет, я этого не делал. Я сказал, это инициализирует возвращаемый объект функции через prvalue. Это не то же самое, что материализовать временное. По сути, A() prvalue инициализирует возвращаемое значение a, которое инициализирует возвращаемое значение b, которое инициализирует объект c. Таким образом, по транзитивному свойству A() инициализирует c. - person Nicol Bolas; 24.05.2017
comment
@ M.M: Я немного перефразировал объяснение. - person Nicol Bolas; 24.05.2017
comment
В случае T var = Func(), если Func возвращает t; (т.е. именованный автоматический), я думаю, что var инициализируется t, сначала путем обработки t как rvalue (и т. Д. По обычным правилам). Так что в некотором смысле здесь задействовано и гарантированное исключение копирования. Но также может применяться обычное исключение копирования, которое может полностью опустить локальный объект функции t и свернуть его в var. - person Johannes Schaub - litb; 27.05.2017
comment
Недавно я обнаружил, что у нас также есть реальные случаи гарантированного исключения копирования в Стандарте: в рамках оценки constexpr компилятор должен применять все исключения копирования (чтобы гарантировать, что указатели, установленные в вызываемом constexpr функции по-прежнему действительны в вызывающей стороне). Однако здесь все же требуется, чтобы конструкторы перемещения / копирования были доступны, поскольку это гарантированный случай copy-elision. Поскольку заголовок вопроса гласит: «Как работает гарантированное исключение копий?», Может быть полезно добавить объяснение и для этого случая. - person Johannes Schaub - litb; 27.05.2017
comment
@ JohannesSchaub-litb: Вопрос ОП включал цитирование конкретного предложения, спрашивая, как оно работает. Они не спрашивают об этом constexpr механизме, о котором вы говорите. - person Nicol Bolas; 27.05.2017
comment
@Nicol Я уточнил заголовок, чтобы было понятно, не глядя в тело - person Johannes Schaub - litb; 27.05.2017
comment
@ JohannesSchaub-litb Я думаю, что редактирование заголовка сбивало с толку; если вы думаете, что этот ответ не полностью отвечает на вопрос, возможно, вы могли бы опубликовать дополнительный ответ, чтобы охватить другие случаи - person M.M; 28.05.2017
comment
Редактирование не касалось этого ответа, и я думаю, что это нормально. Не уверен, почему вы думаете иначе. Вопрос поставлен запутанно. Пожалуйста, оставляйте комментарии к вопросу в соответствующем месте, а не в ответах. - person Johannes Schaub - litb; 28.05.2017
comment
У меня есть не только ощущение (я думаю, что редактирование заголовка сбивало с толку), но также и объективную причину моего изменения - гарантированное исключение копии - термин неоднозначный. - person Johannes Schaub - litb; 28.05.2017
comment
@ JohannesSchaub-litb: Это двусмысленно только в том случае, если вы слишком много знаете о мелочах стандарта C ++. Для 99% сообщества C ++ мы знаем, что означает гарантированное исключение копирования. Настоящий документ, в котором предлагается эта функция, даже имеет заголовок Гарантированное исключение копирования. Добавление с помощью упрощенных категорий значений просто сбивает с толку и затрудняет понимание пользователями. Кроме того, это неправильное название, поскольку эти правила на самом деле не упрощают правила, касающиеся категорий значений. Нравится вам это или нет, но термин "гарантированное копирование" относится к этой функции и ни к чему другому. - person Nicol Bolas; 28.05.2017
comment
Поскольку я, очевидно, в меньшинстве, я откатил редактирование - person Johannes Schaub - litb; 28.05.2017
comment
Я так хочу иметь возможность взять с собой цену и носить ее с собой. Я полагаю, что это всего лишь (одноразовый) std::function<T()> на самом деле. - person Yakk - Adam Nevraumont; 30.05.2017
comment
В формулировке для гарантированного исключения копирования , он упоминает initializer expression несколько раз (например, 8.5 Bullet 17.6), но что такое initializer expression. Я не мог найти ему определения. - person doraemon; 31.07.2018
comment
@LiuSha: Это в самом стандарте; оно уже имеет значение, поэтому в предложении не нужно было его объяснять. Однако это именно то, о чем он говорит: выражение, используемое для инициализации чего-либо. - person Nicol Bolas; 31.07.2018
comment
@NicolBolas Верно ли следующее понимание примера A x = instanceA + returnsA() + returnsRefToA() + returnsRRefToA()? returnsA() - это prvalue, которое материализуется как операнд для +, returnsRefToA() - это lvalue; а returnsRRefToA() - значение x. Но все они используются для инициализации параметра для operator +, должны ли они быть prvalue? Или они glvalue конвертируются в prvalue автоматически? - person doraemon; 01.08.2018
comment
Зависит ли гарантированное копирование от настроек компилятора? - person Robert Andrzejuk; 12.10.2018
comment
@RobertAndrzejuk: Это функция C ++ 17, поэтому вы должны делать все, что требуется вашему компилятору, чтобы получить возможности C ++ 17. Предполагая, что он реализует эту функцию. - person Nicol Bolas; 12.10.2018
comment
Для T t = Func(); вы говорите: Это инициализация копии t. Это скопирует инициализацию t с возвращаемым значением Func. Однако у T по-прежнему должен быть конструктор перемещения, даже если он не будет вызываться ... Я не понимаю, как это правда. Разве Func() не является значением prvalue или xvalue и, следовательно, будет вызываться возможно существующий конструктор перемещения? Например, в: coliru.stacked-crooked.com/a/f3339b1c6604b928 - person user1658887; 11.01.2019
comment
@ user1658887: Это правда, потому что стандарт явно разрешает это правда. Это заявление явно позволяет этому случиться. Это называется копированием. - person Nicol Bolas; 11.01.2019
comment
Извините, я неправильно понял ваше утверждение. Для меня это выглядело так, как если бы вы говорили, что ни при каких обстоятельствах конструктор перемещения не будет вызываться в этом сценарии. Я просто снова прочитал ваш ответ и понял, что это совсем не то, что вы намеревались передать, то есть конструктор перемещения должен присутствовать, даже если он опущен. Виноват. - person user1658887; 11.01.2019
comment
Может кто-нибудь проверить следующий вопрос? Кажется, с этим ответом может быть проблема. stackoverflow.com/questions/56778285/ - person Lukas Salich; 26.06.2019
comment
@LukasSalich: Это вопрос C ++ 11. Этот ответ касается функции C ++ 17. - person Nicol Bolas; 26.06.2019
comment
@NotAZoomedImage: Этот вопрос не имеет ничего общего с гарантированной элизией. Он действительно спрашивает, совпадает ли T t{}; с T t;. - person Nicol Bolas; 29.09.2020
comment
@NotAZoomedImage: не размещайте комментарии, рекламирующие другие ваши вопросы. Кроме того, на этот вопрос уже есть ответ, который я считаю адекватным. Не менее важно, что этот вопрос не тот, что вы только что задали в этом комментарии. - person Nicol Bolas; 29.09.2020
comment
Emplace_back стал неактуальным, если копирование гарантировано? Если вы можете просто вызвать push_back с помощью конструктора tmp obj, разве эта гарантия не будет потеряна? - person Icebone1000; 13.10.2020
comment
@ Icebone1000: Нет, на оба вопроса. Если у него есть имя, такое как имя параметра, оно больше не является prvalue. Гарантированное исключение только применяется к prvalues. - person Nicol Bolas; 13.10.2020
comment
Было бы правильно сказать, что возвращаемое значение Func () и T () в его операторе возврата имеют переменную t в качестве объекта результата? Спасибо за вашу помощь. - person CyberMarmot; 16.11.2020

Думаю, здесь подробно рассказали о копировании. Однако я нашел эту статью: https://jonasdevlieghere.com/guaranteed-copy-elision который относится к гарантированному исключению копирования в C ++ 17 в случае оптимизации возвращаемого значения.

Это также относится к тому, как, используя параметр gcc: -fno-elide-constructors, можно отключить исключение копирования и увидеть, что вместо конструктора, вызываемого напрямую в месте назначения, мы видим 2 конструктора копирования (или конструкторы перемещения в С ++ 11 ) и вызываемые им соответствующие деструкторы. В следующем примере показаны оба случая:

#include <iostream>
using namespace std;
class Foo {
public:
    Foo() {cout << "Foo constructed" << endl; }
    Foo(const Foo& foo) {cout << "Foo copy constructed" << endl;}
    Foo(const Foo&& foo) {cout << "Foo move constructed" << endl;}
    ~Foo() {cout << "Foo destructed" << endl;}
};

Foo fReturnValueOptimization() {
    cout << "Running: fReturnValueOptimization" << endl;
    return Foo();
}

Foo fNamedReturnValueOptimization() {
    cout << "Running: fNamedReturnValueOptimization" << endl;
    Foo foo;
    return foo;
}

int main() {
    Foo foo1 = fReturnValueOptimization();
    Foo foo2 = fNamedReturnValueOptimization();
}
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 testFooCopyElision.cxx # Copy elision enabled by default
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo destructed
Foo destructed
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 -fno-elide-constructors testFooCopyElision.cxx # Copy elision disabled
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Foo destructed
Foo destructed

Я вижу оптимизацию возвращаемого значения. исключение копирования временных объектов в операторах возврата, как правило, гарантируется независимо от C ++ 17.

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

Было бы неплохо, если бы был способ гарантировать исключение копирования или получить какое-то предупреждение, когда удаление копирования не может быть выполнено, что заставило бы разработчиков убедиться, что выполняется исключение копирования, и повторно использовать код, если он не может быть выполнен .

person Vineet Gupta    schedule 19.06.2021