표현식(expression)이 등호 왼쪽에 놓일 수 있으면 lvalue, 놓일 수 없으면 rvalue
lvalue
등호(=)의 왼쪽에 올 수 있음
이름이 있고(데이터 메모리 차지), 단일식을 벗어나서 사용 가능
주소 연산자로 주소를 구할 수 있음
참조를 반환하는 함수, 문자열 literal
rvalue
등호(=)의 왼쪽에 올 수 없음
이름이 없고, 단일식에서만 사용
주소 연산자로 주소를 구할 수 없음
값을 반환하는 함수, 실수/정수 literal, 임시객체(temporary)
int x = 10;
int f1() { return x;} // "10" 을 반환
int& f2() { return x;} // x의 별명 반환
int main()
{
int v1 = 0, v2 = 0;
v1 = 10; // ok v1 : lvalue
// 10 = v1; // error 10 : rvalue
v2 = v1;
int* p1 = &v1; // ok
// int* p2 = &10; // error
// f1() = 20; // 10 = 20 error
f2() = 20; // x = 20 ok
const int c = 10;
// c = 20; // error, 상수는 rvalue가 아님, immutable lvalue
// Point(1,2).set(10,20); // 임시객체는 상수가 아님
10 = 20; // error. 10은 lvalue 가 아님
"aa"[0] = 'x'; // lvalue 문제가 아니라, const char[3]이므로 컴파일 에러
}
int main()
{
int n = 3;
n = 10; // ok
n + 2 = 10; // error
n + 2 * 3 = 10; // error
(n = 20) = 10; // ok
++n = 10; // ok, n이 반환
n++ = 10; // error, 값이 반환
}
#include <type_traits>
#define value_category(...) \
if ( std::is_lvalue_reference_v<decltype((__VA_ARGS__))> ) \
std::cout << "lvalue" << std::endl; \
else if (std::is_rvalue_reference_v<decltype((__VA_ARGS__))>) \
std::cout << "rvalue(xvalue)" << std::endl; \
else \
std::cout << "rvalue(prvalue)" << std::endl;
int main()
{
int n = 10;
value_category(n); // lvalue
value_category(n+2); // prvalue
value_category(++n); // lvalue
value_category(n++); // prvalue
value_category(10); // prvalue
value_category("AA"); // lvalue
}
int main()
{
int n = 3;
int& r1 = n; // ok
int& r2 = 3; // error
const int& r3 = n; // ok
const int& r4 = 3; // ok
// C++11
int&& r5 = n; // error
int&& r6 = 3; // rvalue를 상수성 없이 받을 수 있음
}
상수성 없이 rvalue를 가리키는 것이 중요한 이유는
move semantics와 perfect forwarding을 위해서!
class X{};
//void foo(X x)
{ std::cout << "X" << std::endl;}
// out parameter, 객체를 수정하겠다는 의미
void foo(X& x) // lvalue만 받을 수 있음
{ std::cout << "X&" << std::endl;} // 1
// in parameter, 객체를 읽기만 하겠다는 의미
void foo(const X& x) // lvalue와 rvalue를 모두 받을 수 있음
{ std::cout << "const X&" << std::endl;} // 2
// move semantics를 사용하겠다는 의도
void foo(X&& x) // rvalue만 받을 수 있음
{ std::cout << "X&&" << std::endl;} // 3
// 문법적으로 만들 수 있지만 의미없음, const&에서 처리 가능 (현재 C++에서는 미사용)
void foo(const X&& x) // rvalue만 받을 수 있음
{ std::cout << "const X&&" << std::endl;}
int main()
{
X x;
// foo( x ); // lvalue
// 1번 호출, 없으면 2번
foo( X() ); // rvalue
// 3번 호출, 없으면 2번
}
int main()
{
foo( X() ); // 3
X&& rx = X();
foo(rx); // 1
}
X()의 데이터 타입은 X며 temporory이므로 rvalue
rx의 데이터 타입은 X&&(rvalue reference)지만 이름이 있으므로 lvalue
foo(X&&)는 rvalue reference를 받는 것이 아닌 rvalue를 받겠다는 의미
그래서 foo(rx)는 foo(X&)를 호출
int main()
{
// lvalue => rvalue 캐스팅
foo(static_cast<X&&>(rx)); // 3
}
rx가 이미 X&& 타입이라 static_cast<X&&>(rx)는 같은 타입 캐스팅으로 보이지만,
타입 캐스팅에 '&&'이 붙으면 타입 캐스팅이 아닌 rvalue로 변환하는 캐스팅임
참조를 가리키는 참조 타입
포인터를 가리키는 포인터처럼 참조를 가리키는 참조 변수를 직접 코드로 만들 순 없음
하지만 type deduction(decltype) 과정에서 참조를 가리키는 참조 타입이 발생하면 reference collapsing 규칙에 따라 타입이 결정
reference collapsing 규칙
Type& & -> Type&
Type& && -> Type&
Type&& & -> Type&
Type&& && -> Type&&
int main()
{
int n = 3;
int& lr = n; // lvalue reference
int&& rr = 3; // rvalue reference
// int& & ref2ref = lr; // 컴파일 에러
decltype(lr)& r1 = n; // int& & => int&
decltype(lr)&& r2 = n; // int& && => int&
decltype(rr)& r3 = n; // int&& & => int&
decltype(rr)&& r4 = 3; // int&& && => int&&
}
template<typename T> void foo(T&& arg) {}
int main()
{
int n = 10;
typedef int& LREF;
LREF&& r1 = n; // int& && => int&
using RREF = int&&;
RREF&& r2 = 10; // int&& && => int&&
decltype(r2)&& r3 = 10; // int&& && => int&&
foo<int&>( n ); // foo( int& && arg )
// -> foo( int& arg ) 함수 생성
}
template의 T&&: forwarding(universal) reference 개념