Возврат аргумента, переданного по ссылке rvalue

Если у меня есть класс A и функции

A f(A &&a)
{
doSomething(a);
return a;
}
A g(A a)
{
doSomething(a);
return a;
}

конструктор копирования вызывается при возврате a от f, но конструктор перемещения используется при возврате из g, Однако из того, что я понимаю, f может быть передан только объект, который безопасно перемещать (временный или объект, помеченный как подвижный, например, с использованием std::move). Есть ли пример, когда было бы небезопасно использовать конструктор перемещения при возврате из f? Почему мы требуем a иметь автоматический срок хранения?

Я читаю ответы Вот, но верхний ответ только показывает, что спецификация не должна позволять двигаться при прохождении a другим функциям в теле функции; это не объясняет, почему переезд при возвращении безопасно для g но не для f, Как только мы доберемся до оператора возврата, нам не понадобится a больше внутри f,

Поэтому я понимаю, что временные шкалы доступны до конца полного выражения. Тем не менее, поведение при возвращении из f кажется, все еще идет вразрез с укоренившейся в языке семантикой, что безопасно перемещать временное значение или значение x. Например, если вы звоните g(A())временный перемещается в аргумент для g хотя там могут быть ссылки на временное хранилище где-то. То же самое происходит, если мы позвоним g с xvalue. Поскольку только временные ссылки и значения xvalue связываются со ссылками на rvalue, похоже, что они согласованы с семантикой, которую мы все еще должны перемещать a при возвращении из f, так как мы знаем a был передан либо временный или xvalue.

6

Решение

Вторая попытка Надеюсь, это больше краткий и Чисто.

Я собираюсь игнорировать RVO почти полностью для этой дискуссии. Это действительно сбивает с толку то, что должно произойти без оптимизаций — это почти что семантика перемещения против копирования.

Чтобы помочь этому, ссылка будет очень полезна здесь виды типов значений в C ++ 11.

Когда двигаться?

именующий

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

prvalue

Выше определено их как «выражения, которые не имеют идентичности». Очевидно, что ничто иное не может ссылаться на безымянное значение, поэтому их можно перемещать.

Rvalue

Общий случай «правой» стоимости, и единственное, что точно известно, это то, что они могут быть перемещены из. Они могут иметь или не иметь именованную ссылку, но если они это делают, это последнее такое использование.

xvalue

Это своего рода смесь обоих — они имеют идентичность (это ссылка) а также они могут быть перемещены из. Им не нужно иметь именованную переменную. Причина? Это новые ценности, которые скоро будут уничтожены. Считайте их «окончательной ссылкой». xvalues ​​может быть сгенерировано только из rvalues вот почему / как std::move работает в преобразовании lvalues ​​в xvalues ​​(через результат вызова функции).

glvalue

Другой тип мутанта с его двоюродным братом rvalue, это может быть либо xvalue, либо lvalue — он имеет идентичность, но неясно, является ли это последней ссылкой на переменную / хранилище или нет, следовательно, неясно, может ли он быть перемещен из ,

Порядок разрешения

Там, где существует перегрузка, которая может принять либо const lvalue ref или же rvalue refи передается значение rvalue, значение rvalue связывается, в противном случае используется версия lvalue. (переместить для значений, скопировать в противном случае).

Где это может произойти

(предположим, что все типы A где не упоминается)

Это происходит только тогда, когда объект «инициализируется из значения xvalue того же типа». xvalue связывается с rvalues, но не так ограничен, как чистые выражения. Другими словами, подвижные вещи — это не просто неназванные ссылки, они также могут быть «последней» ссылкой на объект в отношении осведомленности компилятора.

инициализация

A a = std::move(b); // assign-move
A a( std::move(b) ); // construct-move

передача аргумента функции

void f( A a );
f( std::move(b) );

возврат функции

A f() {
// A a exists, will discuss shortly
return a;
}

Почему этого не произойдет в f

Рассмотрим эту вариацию на f:

void action1(A & a) {
// alter a somehow
}

void action2(A & a) {
// alter a somehow
}

A f(A && a) {
action1( a );
action2( a );
return a;
}

Это не незаконно лечить a как ценность внутри f, Потому что это lvalue это должна быть ссылка, явная или нет. Каждая простая старая переменная технически является ссылкой на себя.

Вот где мы спотыкаемся. Так как a является lvalue для целей fмы на самом деле возвращаем lvalue.

Чтобы явно сгенерировать r-значение, мы должны использовать std::move (или создать A&& приведи другой путь).

Почему это произойдет в g

С этим под нашими поясами, рассмотрим g

A g(A a) {
action1( a ); // as above
action2( a ); // as above
return a;
}

Да, a является lvalue для целей action1 а также action2, Тем не менее, потому что все ссылки на a существуют только внутри g (это копия или перемещенная в копию), она может рассматриваться как xvalue в возвращении.

Но почему не в f?

Там нет особой магии &&, На самом деле, вы должны думать об этом как о справочнике в первую очередь. Тот факт, что мы требуем Rvalue ссылка в f в отличие от lvalue ссылка с A& не меняет тот факт, что, будучи ссылкой, он должен быть lvalue, потому что место хранения из a является внешним по отношению к f и это касается любого компилятора.

То же самое не относится к gгде ясно, что aХранилище является временным и существует только тогда, когда g называется и в другое время. В этом случае это явно значение x и может быть перемещено.


rvalue ref против lvalue ref и безопасность передачи справок

Предположим, мы перегружаем функцию, чтобы принять оба типа ссылок. Что случилось бы?

void v( A  & lref );
void v( A && rref );

Единственный раз void v( A&& ) будет использоваться в соответствии с вышеизложенным («Где это может произойти»), в противном случае void v( A& ), Таким образом, ссылка rvalue всегда будет пытаться привязаться к сигнатуре rvalue до попытки перегрузки ссылки lvalue. Ссылка lvalue никогда не должна связываться с ссылкой rvalue, за исключением случая, когда она может рассматриваться как значение xvalue (гарантированно будет уничтожено в текущей области независимо от того, хотим мы этого или нет).

Соблазнительно сказать, что в случае rvalue мы точно знаем, что передаваемый объект является временным. Это не относится к делу. Это подпись предназначен для обязательные ссылки на то, что по-видимому временный объект.

По аналогии, это как делать int * x = 23; — это может быть неправильно, но вы можете (в конце концов) заставить его скомпилировать с плохими результатами, если вы запустите его. Компилятор не может точно сказать, если вы серьезно относитесь к этому или тянете его за ногу.

Что касается безопасности, нужно учитывать функции, которые делают это (и почему бы не сделать это — если он все еще компилируется):

A & make_A(void) {
A new_a;
return new_a;
}

Хотя в языковом аспекте нет ничего плохого — типы работают, и мы получим ссылку на где-то назад — потому что new_aМесто хранения внутри функция, память будет восстановлена ​​/ недействительной, когда функция вернется. Поэтому все, что использует результат этой функции, будет иметь дело с освобожденной памятью.

Так же, A f( A && a ) предназначен, но не ограничивается принятием значений prvalues ​​или xvalue, если мы действительно хотим форсировать что-то еще. Это где std::move приходит, и давайте сделаем это.

Причина в том, что это отличается от A f( A & a ) только в отношении того контекста, который будет предпочтительным, по сравнению с перегрузкой rvalue. Во всем остальном он идентичен тому, как a лечится компилятором.

Тот факт, что мы знать тот A&& это подпись зарезервирована для движения является спорной; он используется для определения какой версии ссылки на A параметр типа «мы хотим связать, вид, где мы должен взять на себя ответственность (rvalue) или вид, где мы не следует принять владение (lvalue) базовых данных (то есть переместить их в другое место и стереть экземпляр / ссылку, которые мы дали). В обоих случаях мы работаем с ссылка на память, которая не контролируется f.

Компилятор может сказать, делаем мы это или нет; он попадает в область «здравого смысла» программирования, например, не использовать области памяти, которые не имеют смысла использовать, но в остальном являются допустимыми областями памяти.

О чем знает компилятор A f( A && a ) это не создавать новое хранилище для a, так как нам будет дан адрес (ссылка) для работы. Мы можем оставить исходный адрес без изменений, но вся идея здесь заключается в том, что A&& мы говорим компилятору: «Эй! дай мне ссылки на объекты, которые скоро исчезнут, чтобы я мог что-то с этим сделать, прежде чем это произойдет». Ключевое слово здесь может быть, и еще раз также тот факт, что мы можем явно указывать на эту сигнатуру функции неправильно.

Подумайте, была ли у нас версия A что при построении перемещения не удалялись данные старого экземпляра, и по какой-то причине мы сделали это специально (скажем, у нас были свои собственные функции выделения памяти и мы точно знали, как наша модель памяти сохранит данные за время существования объектов) ,

Компилятор не может этого знать, потому что потребуется анализ кода, чтобы определить, что происходит с объектами, когда они обрабатываются в привязках rvalue — это вопрос человеческого суждения в этот момент. В лучшем случае компилятор видит «ссылку, да, здесь не нужно выделять дополнительную память» и следует правилам передачи ссылок.

Можно с уверенностью предположить, что компилятор думает: «это ссылка, мне не нужно иметь дело с временем жизни памяти внутри fвременное будет удалено после f закончен».

В том случае, когда временный f, хранилище этого временного исчезнет, ​​как только мы уйдем fи тогда мы потенциально окажемся в той же ситуации, что и A & make_A(void) — очень плохой.

Вопрос семантики …

std::move

Сама цель std::move это создать rvalue ссылки. По большому счету, то, что он делает (если не делает ничего другого), заставляет результирующее значение привязываться к значениям, а не к значениям. Причиной этого является возвратная подпись A& до того, как ссылки на rvalue стали доступны, был неоднозначным для таких вещей, как перегрузки операторов (и, конечно, других применений).

Операторы — пример

class A {
// ...
public:
A & operator= (A & rhs); // what is the lifetime of rhs? move or copy intended?
A & operator+ (A & rhs); // ditto
// ...
};

int main() {
A result = A() + A(); // wont compile!
}

Обратите внимание, что это не будет принимать временные объекты ни для одного оператора! Также не имеет смысла делать это в случае операций копирования объекта — зачем нам нужно изменять исходный объект, которым мы являемся копирование, наверное чтобы есть копия, которую мы можем изменить позже. Это причина, по которой мы должны объявить const A & параметры для операторов копирования и любая ситуация, когда копия должна быть взята из ссылки, как гарантия того, что мы не изменим исходный объект.

Естественно, это проблема с ходами, где мы должен изменить исходный объект, чтобы избежать преждевременного освобождения данных нового контейнера. (следовательно, операция «переместить»).

Чтобы решить этот беспорядок приходит T&& объявления, которые являются заменой вышеприведенного примера кода, и, в частности, предназначаются для ссылок на объекты в ситуациях, когда вышеописанное не будет компилироваться. Но нам не нужно изменять operator+ быть операцией перемещения, и вам будет трудно найти причину для этого (хотя я думаю, что вы могли бы). Опять же, из-за предположения, что сложение не должно изменять исходный объект, только объект левого операнда в выражении. Итак, мы можем сделать это:

class A {
// ...
public:
A & operator= (const A & rhs); // copy-assign
A & operator= (A && rhs); // move-assign
A & operator+ (const A & rhs); // don't modify rhs operand
// ...
};

int main() {
A result = A() + A(); // const A& in addition, and A&& for assign
A result2 = A().operator+(A()); // literally the same thing
}

Что вы должны принять к сведению здесь, что несмотря на дело в том, что A() возвращает временный, он не только способен связать const A& но это должен из-за ожидаемой семантики сложения (что оно не изменяет свой правый операнд). Вторая версия присваивания более понятна, поэтому следует ожидать изменения только одного из аргументов.

Также ясно, что перемещение будет выполнено в задании, и с rhs в operator+,

Разделение семантики возвращаемого значения и семантики привязки аргумента

Причина, по которой выше только один ход, ясна из определения функции (ну, оператора). Важно то, что мы действительно связываем то, что явно является значением xvalue / rvalue, с тем, что явно является lvalue в operator+,

Я должен подчеркнуть этот момент: в этом примере нет эффективной разницы в том, как operator+ а также operator= обратитесь к их аргументу. Что касается компилятора, то внутри тела функции аргумент const A& за + а также A& за =, Разница чисто в constНесс. Единственный способ, которым A& а также A&& Различают различие подписей, а не типов.

С разными сигнатурами существует разная семантика, это инструментарий компилятора, позволяющий различать определенные случаи, когда в остальном нет четкого различия с кодом. Поведение самих функций — тела кода — также не может отличить случаи друг от друга!

Еще один пример этого operator++(void) против operator++(int), Первый рассчитывает вернуть свою базовую стоимость до операция приращения и последнее после. Здесь нет int После этого, компилятор имеет две сигнатуры для работы — просто нет другого способа указать две идентичные функции с одним и тем же именем, и, как вы можете знать, а можете и не знать, перегрузка функции только на тип возврата по аналогичным причинам неоднозначности.

переменные и другие странные ситуации — исчерпывающий тест

Чтобы однозначно понять, что происходит в f Я собрал «шведский стол» вещей, которые «не следует пытаться делать, но они выглядят так, как будто они будут работать», которые почти исчерпывают все усилия компилятора:

void bad (int && x, int && y) {
x += y;
}
int & worse (int && z) {
return z++, z + 1, 1 + z;
}
int && justno (int & no) {
return worse( no );
}
int num () {
return 1;
}
int main () {
int && a = num();
++a = 0;
a++ = 0;
bad( a, a );
int && b = worse( a );
int && c = justno( b );
++c = (int) 'y';
c++ = (int) 'y';
return 0;
}

g++ -std=gnu++11 -O0 -Wall -c -fmessage-length=0 -o "src\\basictest.o" "..\\src\\basictest.cpp"

..\src\basictest.cpp: In function 'int& worse(int&&)':
..\src\basictest.cpp:5:17: warning: right operand of comma operator has no effect [-Wunused-value]
return z++, z + 1, 1 + z;
^
..\src\basictest.cpp:5:26: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
return z++, z + 1, 1 + z;
^
..\src\basictest.cpp: In function 'int&& justno(int&)':
..\src\basictest.cpp:8:20: error: cannot bind 'int' lvalue to 'int&&'
return worse( no );
^
..\src\basictest.cpp:4:7: error:   initializing argument 1 of 'int& worse(int&&)'
int & worse (int && z) {
^
..\src\basictest.cpp: In function 'int main()':
..\src\basictest.cpp:16:13: error: cannot bind 'int' lvalue to 'int&&'
bad( a, a );
^
..\src\basictest.cpp:1:6: error:   initializing argument 1 of 'void bad(int&&, int&&)'
void bad (int && x, int && y) {
^
..\src\basictest.cpp:17:23: error: cannot bind 'int' lvalue to 'int&&'
int && b = worse( a );
^
..\src\basictest.cpp:4:7: error:   initializing argument 1 of 'int& worse(int&&)'
int & worse (int && z) {
^
..\src\basictest.cpp:21:7: error: lvalue required as left operand of assignment
c++ = (int) 'y';
^
..\src\basictest.cpp: In function 'int& worse(int&&)':
..\src\basictest.cpp:6:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^
..\src\basictest.cpp: In function 'int&& justno(int&)':
..\src\basictest.cpp:9:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^

01:31:46 Build Finished (took 72ms)

Это неизмененный выходной заголовок sans build, который вам не нужно видеть 🙂 Я оставлю это в качестве упражнения, чтобы понять обнаруженные ошибки, но перечитывая мои собственные объяснения (особенно в дальнейшем), должно быть очевидно, что каждая ошибка был вызван и почему, IMO в любом случае.

Вывод — что мы можем извлечь из этого?

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

Во-вторых, для операций перемещения преимущественно предусматривались типы значений — весьма специфическое обстоятельство, которое, как ожидали, возникнет при назначении и строительстве объектов. Другая семантика, использующая привязки ссылок rvalue, выходит за рамки возможностей любого компилятора. Другими словами, лучше рассматривать ссылки на rvalue как синтаксический сахар, чем в реальном коде. Подпись отличается A&& против A& но тип аргумента для целей тела функции нет, он всегда рассматривается как A& с намерение что объект передается должен быть изменены каким-то образом, потому что const A&, хотя и синтаксически корректный, не позволил бы желаемое поведение.

Я могу быть очень уверен в этом, когда скажу, что компилятор сгенерирует тело кода для f как будто это было объявлено f(A&), За выше, A&& помогает компилятору выбирать, когда разрешать привязка изменяемой ссылки к f но в противном случае компилятор не учитывает семантику f(A&) а также f(A&&) отличаться от того, что f возвращается.

Это долгий способ сказать: метод возврата f не зависит от типа аргумента, который он получает.

Путаница — это избрание. На самом деле в возвращении значения есть две копии. Сначала копия создается как временная, затем этот временный объект назначается чему-либо (или не является и остается чисто временным). второй копия, скорее всего, удаляется с помощью оптимизации возврата. первый копия может быть перемещена в g и не может в f, Я ожидаю в ситуации, когда f не может быть исключен, будет копия, а затем перейти от f в оригинальном коде.

Чтобы переопределить это, временный объект должен быть явно создан с использованием std::moveв выражении возврата в f, Однако в g мы возвращаем что-то, что, как известно, является временным для тела функции gследовательно, он либо перемещается дважды, либо перемещается один раз, затем удаляется.

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

7

Другие решения

Короткая история: это зависит только от doSomething,

Средняя история: если doSomething никогда менять a, затем f безопасно. Он получает ссылку на rvalue и возвращает новое временное значение, перемещенное оттуда.

Длинная история: все пойдет плохо, как только doSomething использования a в операции перемещения, потому что a может находиться в неопределенном состоянии, прежде чем он будет использован в операторе возврата — он будет таким же в g но по крайней мере преобразование в ссылку на rvalue должно быть явным

TL / DR: оба f а также g безопасны до тех пор, пока внутри нет операции перемещения doSomething, Разница в том, что ход будет молча выполнен в f, в то время как это потребует явного преобразования в rvalue ссылку (например, с std::move) в г.

0

Третья попытка Второе стало очень длинным в процессе объяснения каждого закоулка ситуации. Но эй, я тоже многому научился в процессе, что, я полагаю, в этом смысл, нет? 🙂 Тем не мение. Я перейду к вопросу заново, оставив мой более длинный ответ, поскольку он сам по себе является полезным справочным материалом, но не дает «четкого объяснения».

f а также g не тривиальные ситуации. Им нужно время, чтобы понять и оценить первые несколько раз, когда вы сталкиваетесь с ними. Проблемы в игре являются время жизни объектов, Оптимизация возвращаемого значения, путаница возврат значений объекта, и путаница с перегрузки ссылочных типов. Я обращусь к каждому и объясню их актуальность.

Рекомендации

Первым делом первым. Какая ссылка? Разве они не просто указатели без синтаксиса?

Они есть, но, что важно, они гораздо больше, чем это. Указатели буквально таковы, они относятся к областям памяти в целом. Есть несколько гарантий относительно значений, расположенных там, где установлен указатель. Ссылки с другой стороны привязаны к адресам реальных ценностей — ценностей, которые гарантируют существование на время они могут быть доступны, но может не иметь имени для них, доступного для доступа любым другим способом (таким как временные).

Как правило, если вы можете «взять его адрес», то имеете дело со ссылкой, довольно специальной, известной как lvalue, Вы можете назначить на lvalue. Вот почему *pointer = 3 работает, оператор * создает ссылку на указанный адрес.

Это не делает ссылку более или менее действительной, чем адрес, на который она указывает, однако ссылается на вас естественно найти в C ++ есть такая гарантия (как и в хорошо написанном коде C ++) — что они ссылаются на реальные значения таким образом, что нам не нужно знать о его времени жизни во время нашего взаимодействия с ними.

Время жизни объектов

Мы все должны знать, когда c’ors и d’ors будут вызываться для чего-то вроде этого:

{
A temp;
temp.property = value;
}

tempсфера действия установлена. Мы точно знаем, когда он был создан и уничтожен. Мы можем быть уверены, что он уничтожен, потому что это невозможно:

A & ref_to_temp = temp; // nope
A * ptr_to_temp = &temp; // double nope

Компилятор останавливает нас от этого, потому что очень очевидно, мы не должны ожидать, что этот объект все еще существует. Это может возникнуть тонко всякий раз, когда используются ссылки, именно поэтому иногда можно встретить людей, предлагающих избегать ссылок, пока вы не узнаете, что вы с ними делаете (или полностью, если они перестали понимать их и просто хотят продолжать жить).

Объем выражений

С другой стороны, мы также должны помнить, что временные существуют до тех пор, пока не завершится самое внешнее выражение, в котором они находятся. Это значит до точки с запятой. Например, выражение, существующее в LHS оператора запятой, не уничтожается до точки с запятой. То есть:

struct scopetester {
static int counter = 0;
scopetester(){++counter;}
~scopetester(){--counter;}
};

scopetester(), std::cout << scopetester::counter; // prints 1
scopetester(), scopetester(), std::cout << scopetester::counter; // prints 2

Это все еще не позволяет избежать проблем последовательности выполнения, вам все равно придется иметь дело с ++i++ и другие вещи — операторский приоритет и страшные неопределенное поведение это может привести к возникновению неоднозначных случаев (например, i++ = ++i). Важно то, что все созданные временные источники существуют до точки с запятой и больше не существуют.

Есть два исключения — elision / in-place-construction (он же RVO) и справочно-присвоения из-временного.

Возвращение по значению и Elision

Что такое elision? Зачем использовать RVO и подобные вещи? Все это сводится к одному термину, который гораздо проще оценить — «строительство на месте». Предположим, мы использовали результат вызова функции для инициализации или установки объекта. Например:

A x (void) {return A();}
A y( x() );

Давайте рассмотрим самую длинную последовательность событий, которая может произойти здесь.

  1. Новый A построен в x
  2. Временное значение, возвращаемое x() это новый A, инициализированный с использованием ссылки на предыдущий
  3. Новый Ay — инициализируется с использованием временного значения

Где возможно, компилятор должен переставлять вещи так, чтобы как можно меньше промежуточных Aпостроены там, где можно предположить промежуточный недоступен или иным образом не нужен. Вопрос в том который из объектов мы можем обойтись без?

Случай № 1 является явным новым объектом. Если мы хотим избежать этого, нам нужна ссылка на объект, который уже существует. Это самый простой и больше ничего не нужно говорить.

В # 2 мы не можем избежать построения немного результат. Ведь мы возвращаемся по значению. Однако есть два важных исключения (не включая сами исключения, которые также влияет при броске): NRVO а также РВО. Они влияют на то, что происходит в # 3, но есть важные последствия и правила относительно № 2 …

Это связано с интересным Причудливость:

Заметки

Исключение копирования — единственная разрешенная форма оптимизации, которая может изменить наблюдаемые побочные эффекты. Поскольку некоторые компиляторы не выполняют удаление копии в каждой ситуации, где это разрешено (например, в режиме отладки), программы, которые полагаются на побочные эффекты конструкторов и деструкторов копирования / перемещения, не являются переносимыми.

Даже когда удаление копии происходит и конструктор копирования / перемещения не вызывается, он должен присутствовать и быть доступным (как если бы оптимизация вообще не происходила), в противном случае программа не сформирована.

(Начиная с C ++ 11)

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

И еще об этом в заметки о возврате:

Заметки

Возврат по значению может включать в себя создание и копирование / перемещение временного объекта, если не используется разрешение копирования.

(Начиная с C ++ 11)

Если expression является выражением lvalue, и условия для исключения копии соблюдены или будут соблюдены, за исключением того, что expression присваивает имя параметру функции, затем разрешение перегрузки для выбора конструктора, который будет использоваться для инициализации возвращаемого значения, выполняется дважды: сначала, как будто expression были выражением rvalue (таким образом, он может выбрать конструктор перемещения или конструктор копирования со ссылкой на const), и если подходящее преобразование недоступно, разрешение перегрузки выполняется во второй раз с выражением lvalue (поэтому он может выбрать конструктор копирования ссылка на неконстантную).

Приведенное выше правило применяется, даже если тип возвращаемого значения функции отличается от типа expression (копия elision требует того же типа)

Компилятору разрешено даже объединять несколько вариантов. Все это означает, что две стороны перемещения / копии, которые будут включать промежуточный объект, потенциально могут быть сделаны так, чтобы ссылаться непосредственно друг на друга, или даже могут быть сделаны как один и тот же объект. Мы не знаем и не должен знать когда компилятор решит это сделать — это, с одной стороны, оптимизация, но, что важно, вы должны думать о переносе и копировании конструкторов и др. как об использовании «последней инстанции».

Мы можем согласиться, что цель состоит в том, чтобы уменьшить количество ненужных операций при любой оптимизации, при условии, что наблюдаемое поведение одинаково. Используются конструкторы перемещения и копирования где бы ни происходило перемещение и копирование, так что, когда компилятор сочтет целесообразным удалить саму операцию перемещения / копирования в качестве оптимизации? Если функционально ненужный промежуточные объекты существуют в конечной программе только для целей их побочных эффектов? То, как сейчас работает стандарт, и компиляторы, похоже, таковы: нет — конструкторы перемещения и копирования удовлетворяют как из этих операций, а не когда или же Зачем.

Короткая версия: у вас меньше временных объектов, о которых вам не следует беспокоиться с самого начала, так почему вы должны их пропустить. Если вы их пропускаете, возможно, ваш код опирается на промежуточные копии и движется для выполнения задач, выходящих за рамки заявленной цели и контекста.

Наконец, вы должны знать, что объект исключения всегда хранится (и создается) в получение местоположение, а не место его возникновения.

квотирование эта ссылка

Оптимизация именованного возвращаемого значения

Если функция возвращает тип класса по значению, а выражение возвращаемого оператора — это имя энергонезависимого объекта с автоматической продолжительностью хранения, который не является параметром функции или параметром предложения catch и имеет тот же тип ( игнорируя cv-квалификацию верхнего уровня) как возвращаемый тип функции, тогда копирование / перемещение опускается. Когда этот локальный объект создается, он создается непосредственно в хранилище, куда в противном случае было бы перемещено или скопировано возвращаемое значение функции. Этот вариант исключения копии известен как NRVO, «оптимизация именованного значения».

Оптимизация возвращаемого значения

Когда безымянный временный объект, не связанный с какими-либо ссылками, будет перемещен или скопирован в объект того же типа (без учета cv-квалификации верхнего уровня), копирование / перемещение будет опущено. Когда этот временный объект создается, он создается непосредственно в хранилище, куда в противном случае он был бы перемещен или скопирован. Когда безымянный временный аргумент является аргументом оператора возврата, этот вариант разрешения копирования известен как RVO, «оптимизация возвращаемого значения».

Время жизни ссылок

Одна вещь, которую мы не должны делать, это:

A & func() {
A result;
return result;
}

Хотя это заманчиво, потому что это позволит избежать неявного копирования чего-либо (мы просто передаем адрес правильно?), Это также недальновидный подход. Запомните приведенный выше компилятор, чтобы он не выглядел как temp? То же самое здесь — result ушел, как только мы закончили func, это может быть исправлено и может быть что угодно сейчас.

Причина, по которой мы не можем, заключается в том, что мы не можем передать адрес result снаружи func — в качестве ссылки или указателя — и считать его действительной памятью. Мы не получили бы больше прохождения A* из.

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

Однако есть особый случай, о котором говорилось ранее.

Напомним, что ссылки гарантии к реальным ценностям. Это подразумевает, что первый появление ссылки инициализирует объект и прошлой (насколько известно во время компиляции) уничтожает его при выходе из области видимости.

В целом это охватывает две ситуации: когда мы возвращаем временный от функция. и когда мы назначать из результата функции. Первый, возвращающий временное значение, в основном то, что делает elision, но вы можете в действительности явно указать elide с передачей ссылки — как передача указателя в цепочке вызовов. Он создает объект во время возврата, но после изменения области видимости объект больше не уничтожается (оператор return). А на другом конце происходит второй вид — переменная, хранящая результат вызова функции, теперь имеет честь уничтожить значение, когда Это выходит за рамки.

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

Перегрузки ссылочных типов

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

Обычные переменные, когда они покидают область видимости, имеют единственную ссылку на них, исчезают и в это время быстро уничтожаются. Ссылочные переменные могут ссылаться на обычные переменные, но, за исключением случаев elision / RVO, они не влияют на область действия исходного объекта — даже если объект, на который они ссылаются, рано выходит из области видимости, что может произойти, если вы сделаете ссылки на динамическую память и не осторожны, чтобы управлять этими ссылками самостоятельно.

Это означает, что вы можете получить результаты выражения явно по ссылке. Как? Ну, это может показаться странным на первый взгляд, но если вы прочитаете выше, будет понятно, почему это работает:

class A {
/* assume rule-of-5 (inc const-overloads) has been followed but unless
* otherwise noted the members are private */
public:
A (void) { /* ... */ }
A operator+ ( const A & rhs ) {
A res;
// do something with `res`
return res;
}
};

A x = A() + A(); // doesn't compile
A & y = A() + A(); // doesn't compile
A && z = A() + A(); // compiles

Зачем? В чем дело?

A x = ... — мы не можем, потому что конструкторы и назначение являются частными.

A & y = ... — мы не можем, потому что мы возвращаем значение, а не ссылку на значение, область которого больше или равна нашей текущей области.

A && z = ... — мы можем, потому что мы можем ссылаться на xvalues. Как следствие этого присваивания время жизни временного значения расширяется до этого захватывающего lvalue, потому что оно фактически стало ссылкой lvalue. Звучит знакомо? Это явное решение если бы я это называл. Это становится более очевидным, если учесть, что этот синтаксис должен включать новое значение и должен включать присвоение этого значения ссылке.

Во всех трех случаях, когда все конструкторы и присваивания становятся открытыми, всегда создается только три объекта с адресом res всегда соответствует переменной, хранящей результат. (в любом случае на моем компиляторе оптимизации отключены, -std = gnu ++ 11, g ++ 4.9.3).

Это означает, что различия действительно сводятся только к продолжительности хранения самих аргументов функции. Операции Elision и Move не могут происходить ни с чем, кроме чистых выражений, значений с истекающим сроком действия или явного таргетинга ссылочной перегрузки «истекающие значения». Type&&,

Пересматривая f а также g

Я прокомментировал ситуацию в обеих функциях, чтобы ускорить процесс, краткий список предположений, которые компилятор будет замечать при создании (многоразового) кода для каждой из них.

A f( A && a ) {
// has storage duration exceeding f's scope.
// already constructed.

return a;
// can be elided.
// must be copy-constructed, a exceeds f's scope.
}

A g( A a ) {
// has storage duration limited to this function's scope.
// was just constructed somehow, whether by elision, move or copy.

return a;
// elision may occur.
// can move-construct if can't elide.
// can copy-construct if can't move.
}

Что мы можем сказать наверняка о f«s a является то, что он ожидает переместить значения или значения типа выражения. Так как f может принимать либо выражения-ссылки (prvalues), либо lvalue-ссылки, которые могут исчезнуть (xvalues), либо перемещенные lvalue-ссылки (преобразованные в xvalues ​​через std::move), и потому что f должен быть однородным в лечении a для всех трех случаев, a рассматривается как ссылка прежде всего на область памяти, чья жизнь существует дольше, чем призыв к f, То есть невозможно определить, какой из трех случаев, которые мы назвали f с изнутри fтаким образом, компилятор принимает наибольший срок хранения, который ему необходим для любого из случаев, и считает самым безопасным не предполагать что-либо о продолжительности хранения aданные.

В отличие от ситуации в g, Вот, a — однако это происходит по его стоимости — перестанет быть доступным после вызова g, Как таковое возвращение, это равносильно его перемещению, поскольку в этом случае оно рассматривается как значение x. Мы все еще можем скопировать его или, более вероятно, даже исключить его, это может зависеть от того, что разрешено / определено для A в это время.

Проблемы с f

// we can't tell these apart.
// `f` when compiled cannot assume either will always happen.
// case-by-case optimizations can only happen if `f` is
// inlined into the final code and then re-arranged, or if `f`
// is made into a template to specifically behave differently
// against differing types.

A case_1() {
// prvalues
return f( A() + A() );
}

A make_case_2() {
// xvalues
A temp;
return temp;
}
A case_2 = f( make_case_2() )

A case_3(A & other) {
// lvalues
return f( std::move( other ) );
}

Из-за неоднозначности использования компилятор и стандарты предназначены для f можно использовать последовательно во всех случаях. Там не может быть никаких предположений, что A&& всегда будет новым выражением или что вы будете использовать его только с std::move за свои аргументы и т. д. f становится внешним по отношению к вашему коду, оставляя только свою подпись вызова, которая больше не может быть оправданием. Сигнатура функции — какая ссылка перегружает целевой объект — указывает на то, что функция должна делать с ней и сколько (или мало) она может предположить о контексте.

Ссылки на значения не являются панацеей для нацеливания только на «перемещенные значения», они могут нацеливаться на гораздо большее количество вещей и даже нацеливаться неправильно или неожиданно, если вы предполагаете, что это все, что они делают. Ссылка на что-нибудь в общем случае следует ожидать, что он будет существовать дольше, чем ссылка, за исключением того, что rvalue ссылочные переменные.

Значения ссылочных переменных по существу операторы elision. Где бы они ни существовали, на месте ведется какое-то описание.

Как обычные переменные, они расширяют область действия любого xvalue или rvalue, которое они получают — они содержат результат выражения в том виде, как он создан, а не путем перемещения или копирования, и оттуда эквивалентны обычным ссылочным переменным в использовании.

Как переменные функции они могут также исключать и создавать объекты на месте, но между ними есть очень важное различие:

A c = f( A() );

и это:

A && r = f( A() );

Разница в том, что нет никакой гарантии, что c будет построено движение против элиты, но r определенно будет разрешено / построено на месте в какой-то момент, в силу характера того, что мы связываем в. По этой причине мы можем назначить только r в ситуациях, когда будет создано новое временное значение.

Но почему A&&a не уничтожен ли он в плен?

Учти это:

void bad_free(A && a) {
A && clever = std::move( a );
// 'clever' should be the last reference to a?
}

Это не сработает. Причина тонкая. aобласть действия длиннее, и rvalue справочные назначения могут только простираться жизнь, а не контролировать ее. clever существует меньше времени, чем aи, следовательно, не является само значением xvalue (если не используется std::move снова, но затем вы возвращаетесь к той же ситуации, и она продолжается и т.д.).

продление жизни

Помните, что то, что делает l-значения отличными от r-значений, заключается в том, что они не могут быть связаны с объектами, которые имеют меньший срок службы, чем они сами. Все ссылки lvalue являются либо исходной переменной, либо ссылкой, у которой меньше время жизни, чем у исходной.

Значения позволяют связывать ссылочные переменные, которые имеют дольше Время жизни, чем первоначальное значение — это половина дела. Рассматривать:

A r = f( A() ); // v1
A && s = f( A() ); // v2

Что просходит? В обоих случаях f дается временное значение, которое переживает вызов, и объект результата (потому что f возвращается по значению) построен как-то (это не имеет значения, как вы увидите). В v1 мы строим новый объект r используя временный результат — мы можем сделать это тремя способами: переместить, скопировать, убрать. В v2 мы не создаем новый объект, мы продлеваем время жизни результата f в объеме sальтернативно говоря то же самое: s построен на месте с использованием f и, следовательно, временный возвращается f его срок службы увеличен, а не перемещен или скопирован.

Основным отличием является v1 требует перемещать и копировать конструкторы (хотя бы один) для определения даже если процесс исключен. Для v2 вы не вызываете конструкторы и явно говорите, что хотите сослаться и / или продлить время жизни временного значения, и так как вы не вызываете конструкторы перемещения или копирования, которые может компилятор только elide / построить на месте!

Помните, что это не имеет ничего общего с аргументом, данным f, Работает одинаково с g:

A r = g( A() ); // v1
A && s = g( A() ); // v2

g создаст временное для его аргумента и создаст его, используя A() для обоих случаев. Это как f также создает временное для его возвращаемого значения, но он может использовать xvalue, потому что результат создается с использованием временного g). Опять же, это не будет иметь значения, потому что в v1 у нас есть новый объект, который может быть создан с помощью копирования или перемещения (или требуется, но не оба), в то время как в v2 мы требуем ссылку на что-то, что построено, но исчезнет, ​​если мы не будем поймать это

Явный захват xvalue

Пример, демонстрирующий это, возможен в теории (но бесполезен):

A && x (void) {
A temp;
// return temp; // even though xvalue, can't do this
return std::move(temp);
}
A && y = x(); // y now refers to temp, which is destroyed

Какой объект делает y Ссылаться на? Мы не оставили компилятору выбора: y должен ссылаться на результат некоторой функции или выражения, и мы дали ему temp который работает на основе типа. Но никакого движения не произошло, и temp будет освобожден к тому времени, когда мы используем его через y,

Почему не продлилось продление жизни temp как это было для a в g / f? Из-за того, что мы возвращаем: мы не можем указать функцию для создания вещей на месте, мы можем указать переменную для быть построен на месте. Это также показывает, что компилятор не просматривает границы функций / вызовов для определения времени жизни, он просто смотрит, какие переменные находятся на вызывающей стороне или локальные, как они назначены и как они инициализируются, если они локальные.

Если вы хотите избавиться от всех сомнений, попробуйте передать это как ссылку на значение: std::move(*(new A)) — что должно произойти, это то, что ничто никогда не должно уничтожать это, потому что оно не находится в стеке и потому что ссылки на rvalue не изменяют время жизни чего-либо, кроме временных объектов (то есть промежуточных / выражений). Значения x являются кандидатами на создание перемещения / назначение перемещения и не могут быть исключены (уже созданы), но все другие операции перемещения / копирования теоретически могут быть исключены по желанию компилятора; при использовании ссылок rvalue у компилятора нет другого выбора, кроме как исключить или передать адрес.

0
По вопросам рекламы [email protected]