возвращаемое значение rvalue-reference

Рейтинг: 0Ответов: 2Опубликовано: 10.04.2023
int n() {// rvalue returns
    int b = 9;
    return b;
}

int& n1() {// lvalue-ref=lvalue expr
    int b = 9;
    return b;
}

int&& n2() {// rvalue-ref=rvalue expr
    return 9;
}

int main(){
   int y=200;

   n1() = y;// почему здесь rvalue можно присвоить
 
   n2() = 8;// а тут rvalue 

   n() = 0;// и тут rvalue уже нельзя присвоить
   int&& v = 8;}

Из функций получается всегда возвращаются rvalue значения, тк без имени, но которые могут быть разных типов. Почему так непонятно работает?

Ответы

▲ 2Принят

Во-первых, ваши функции n1 и n2 возвращают битые ссылки, потому что объекты, на которые они указывают, умирают при выходе из функции.

Поэтому тот вариант присваивания, который компилируется, все равно вызывает неопределенное поведение.


Из функций получается всегда возвращаются rvalue значения, тк без имени

Неверно. Правило работает наоборот: любое имя - lvalue. Но бывают lvalue и без имен.

n1() возвращает lvalue.

n() и n2() возвращают rvalue. (Eсли точнее, они возвращают prvalue и xvalue соотвественно - это два подвида rvalue. Но здесь между ними похоже нет никакой разницы.)


Как в этом убедиться? Вот так.

#include <iostream>

template <typename T>
void PrintType()
{
    std::cout <<
    #ifdef _MSC_VER
    __FUNCSIG__
    #else
    __PRETTY_FUNCTION__
    #endif
    << '\n';
}
PrintType<decltype((n()))>(); // void PrintType() [T = int]
PrintType<decltype((n1()))>(); // void PrintType() [T = int &]
PrintType<decltype((n2()))>(); // void PrintType() [T = int &&]

А дальше считаете амперсанды в выводе. = prvalue, & = lvalue, && = xvalue.

Обратите внимание на двойные скобки после decltype. Здесь они не влияют на ответ, но в других случаях могут влиять. (Например, int &&x = 42;, decltype(x) = int &&, но decltype((x)) = int &. Здесь x, как ни странно, lvalue, ведь у него есть имя.)


Из вашего примера можно сделать вывод, что rvalue нельзя ничего присваивать, а lvalue можно.

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

Вывод - такой запрет - это искусственное ограничение, защита от выстрела себе в ногу. У lvalue, как и у rvalue, есть адреса. То, что у rvalue нельзя его взять напрямую - это тоже защита от дурака.

(Тут еще нужно объяснение про то, что у prvalue строго говоря адресов нет, и это вообще не объекты - но это занудство, и тут не влияет на результат. Можете почитать про mandatory copy elision.)


int&& v = 8;

У меня ощущение, что тут был немой вопрос - "как здесь получилось присвоить rvalue?".

Ответ - это вообще не присваивание, это инициализация.

Присваиванием было бы int &&v = 8; v = 42;. А вот это компилируется, потому что v - lvalue (выше объяснил почему). Если превратить его в rvalue через std::move, то перестанет компилироваться: std::move(v) = 42;.

▲ 2

Любые объекты, у которых закончилось время жизни теряют и ликвидность ссылки. Неважно & или && - они уже битые. Единственным правильным способом передать умирающий объект остаётся только по значению.

int& n1() {// lvalue-ref=lvalue expr
  int b = 9;
  // здесь b - lvalue
  return b;
  // здесь ссылка уже на битый объект
  // warning: reference to local variable 'b' returned [-Wreturn-local-addr]
}

Здесь компилятор разрешает передавать по l-value ссылке, так как иногда он бывает неспособен вычислить у какого объекта заканчивается время жизни, а если догадывается, то возвращает только предупреждение.

int&& n2() {// rvalue-ref=rvalue expr
  // warning: returning reference to temporary [-Wreturn-local-addr]
  return 9;
}

То же самое здесь, значение по этой ссылке может и очень вероятно будет иметь совсем другое значение. r-ссылка не гарантирует жизнь объекту.
Если функция посложнее и компилятор не видит что объект передаётся уже с истёкшим сроком жизни, то тогда предупреждений не будет и всё зависит от алгоритма.

Если у объекта уже закончилось время жизни r-value ссылке можно подключить только r-value объект после получения значения из функции. И это называется продление жизни возвращаемому значению. Но не r-ссылке.

int n() {// rvalue returns
    int b = 9;
    return b;
}
int main(){
  int && rv = n ( ) ;
  // ссылка rv живая
  ++ rv ;
  int && v = 8;
  // ссылка v живая
  ++ v ;
}

Чтобы оператор присваивания принимал изменения значения объекту нужно его привязать к переменной. Тогда это переменная как r-value ссылка будет иметь статус l-value.

int main(){
  int && rv = n ( ) ;
  // ссылка rv живая, и она имеет статус l-value
  rv = 7 ;
  int && v = 8;
  // ссылка v живая, и она имеет статус l-value
  v = 77 ;
}