Возврат аргумента, переданного по ссылке 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,

Обновление 0

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

3 ответа

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

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

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

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

именующий

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

prvalue

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

Rvalue

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

xvalue

Это своего рода смесь обоих - они имеют идентичность (являются ссылками) и их можно перемещать. Им не нужно иметь именованную переменную. Причина? Это новые ценности, которые скоро будут уничтожены. Считайте их "окончательной ссылкой". Значения xvalue могут быть получены только из значений 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 станет фактором. Даже если я ошибаюсь, неоптимизированная трассировка используемых конструкторов / операций нарисовала бы однозначную картину того, что сделал компилятор, надеюсь, будет понятно, почему он сделал то же, что и он...

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

С чем мы здесь имеем дело?

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(aka RVO) иссылка-назначение-из-временного.

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

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

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

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

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

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

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

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

Это связано с интересной особенностью решения:

Заметки

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

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

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

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

И еще об этом в примечаниях к обратному заявлению:

Заметки

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

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

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

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

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

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

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

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

Цитирую эту ссылку -

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

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

Значения 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 являются либо исходной переменной, либо ссылкой, у которой меньше время жизни, чем у исходной.

Значения r позволяют связывать ссылочные переменные с более длительным временем жизни, чем исходное значение - это половина вопроса. Рассматривать:

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

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

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

Помните, что это не имеет ничего общего с аргументом, данным 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 у компилятора нет другого выбора, кроме как исключить или передать адрес.

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

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

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

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

Другие вопросы по тегам