Lvalue와 Rvalue
C++에서 lvalue는 특정 메모리 위치를 가리킨다. 반면 rvalue는 아무데도 가리키지 않는다. 일반적으로 rvalue는 일시적이고 수명이 짧지만 lvalue는 변수로 존재하기 때문에 수명이 더 길다.
int x = 666; // ok
여기서 666은 rvalue이다. 숫자(엄밀히 말하자면 리터럴 상수)는 프로그램 실행 중의 임시 레지스터를 제외하면 특정 메모리 주소가 없다. 666은 변수 x에 할당된다. 변수는 특정 메모리 주소가 있으며 따라서 lvalue이다. C++에서 할당 연산자(assignment operation)인 =는 왼쪽 피연산자(left operand)로 lvalue가 필요하다.
그리고 lvalue인 x를 가지고 다음과 같은 것을 할 수 있다.
int *y = &x; // ok
여기서 x의 주소를 참조 연산자인 &로 y에 할당하고 있다. &는 lvalue를 인수로 받아 rvalue를 만든다. 할당 연산자 =의 왼쪽에 lvalue인 변수, 오른쪽에 & 연산으로 만들어진 rvalue가 있기 때문에 합법적인 연산이다.
하지만, 다음은 그렇지 않다.
int y;
666 = y; // error!
666은 리터럴 상수이고 rvalue이기 때문에 특정 메모리 주소가 없다. y를 없는 곳에 할당하고 있는 것이다.
프로그램을 컴파일하면 GCC는 이렇게 말한다.
error: lvalue required as left operand of assignment
=연산자의 왼쪽에는 항상 lvalue가 필요하지만 이 코드는 rvalue인 666을 왼쪽 피연산자로 사용하고 있기 때문이다.
마찬가지로 아래의 코드도 불가능하다.
int* y = &666; // error: lvalue required as unary '&' operand
& 연산자는 lvalue를 취해야 하는데, 이는 오직 lvalue만이 참조할 수 있는 주소를 갖고 있기 때문이다.
lvalue와 rvalue를 반환하는 함수
할당 연산자의 왼쪽 피연산자에 lvalue가 와야한다는 것을 알고 있기 때문에, 아래와 같은 코드는 오류가 발생할 것이라는 걸 알 것이다.
int setValue()
{
return 6;
}
// ... somewhere in main() ...
setValue() = 3; // error!
setValue()는 rvalue(숫자 6)를 반환한다. 이는 할당 연산자의 왼쪽 피연산자로 사용할 수 없기 때문에 오류가 발생하는 것은 아주 명백하다.
그렇다면 lvalue를 반환하는 함수는 어떨까?
int global = 100;
int& setGlobal()
{
return global;
}
// ... somewhere in main() ...
setGlobal() = 400; // ok
setGlobal()은 참조(reference)를 반환한다. 참조는 존재하고 있는 메모리 장소(global 변수)를 가리키는 것이다. 이것은 lvalue이기 때문에 = 연산자로 할당할 수 있는 것이다.
함수에서 lvalue를 반환하는 것은 혼란스러워 보이지만 일부 오버로드된 연산자를 구현하는 것과 같은 고급 작업을 할 때 유용하다.
lvalue에서 rvalue로의 변환
lvalue는 rvalue로 변환될 수 있다. 예를 들어 더하기 연산자인 +를 보자. 두 개의 rvalue를 취하고 rvalue를 반환한다.
int x = 1;
int y = 3;
int z = x + y; // ok
x와 y는 lvalue이지만 더하기 연산자는 rvalue를 취한다. 어떻게 된걸까? x와 y는 암시적으로 lvalue에서 rvalue로 변환된 것이다. (lvalue-to-rvalue conversion) 뺄셈, 덧셈, 나눗셈 등 다른 많은 연산자들이 이러한 변환을 수행한다.
lvalue 참조
반대는 어떨까? rvalue를 lvalue로 변활할 수 있을까? 기술적인 제한이 있는것은 아니지만, 불가능하다. 왜냐하면 C++가 그렇게 설계되었기 때문이다. C++에서 다음과 같은 작업을 할 때를 살펴보자.
int y = 10;
int& yref = y;
yref++; // y is now 11
yref는 int& 타입으로, y에 대한 참조이다. 이것을 lvalue 참조(lvalue reference)라고 부른다. 이제 yref를 통해 y의 값을 변경할 수 있다.
참조는 특정 메모리 주소에 이미 존재하고 있는 오브젝트를 가리켜야하고, 여기서 y가 그 오브젝트이기 떄문에, 코드는 아무 결함없이 작동한다.
이 과정을 생략하고 10을 바로 참조하게 하면 어떻게 될까?
int& yref = 10; // will it work?
오른쪽에는 임시적인, lvalue에 저장해야하는 rvalue가 있다.
왼쪽에는 존재하는 오브젝트를 가리켜야 하는 참조(lvalue)가 있다. 하지만 10은 숫자 상수이고 특정 메모리 주소가 없는 rvalue이기 때문에, 이 코드는 참조의 개념과 충돌한다.
이것이 rvalue에서 lvalue로의 금지된 변환이다. 휘발성 숫자 상수(rvalue)를 참조하기 위해선 숫자 상수가 lvalue가 되어야한다. 이것이 허용된다면, 참조를 통해 숫자 상수의 값을 변경할 수 있을 것이다. 별로 의미가 없지 않은가? 또, 숫자값에 대한 참조가 사라지면 어떻게 할 것인가?
다음 코드는 비슷한 이유로 오류가 발생한다.
void func(int& x)
{
}
int main()
{
func(10); // Nope!
// 대신에 아래는 작동한다.
// int x = 10;
// func(x);
}
임시 rvalue인 10을 함수에 매개변수로 넘겨주고 있다. 함수는 참조를 매개변수로 받고 있다. 잘못된 rvalue에서 lvalue로의 변환이다. 이를 해결하려면 rvalue를 저장하는 변수 x를 만들어 이것을 함수에 넘겨주면 된다.
상수 lvalue 참조
GCC로 돌려봤을 때 위의 두 코드 스니펫의 오류는 다음과 같다.
error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
GCC는 상수가 아닌것에 대해 오류라고 말하고 있다. 언어 명세(language specifications)에 따라 const lvalue를 rvalue에 바인딩할 수 있다. 아래 코드는 마법같이 작동한다.
const int& ref = 10; // OK!
void func(const int& x)
{
}
int main()
{
func(10); // OK!
}
리터럴 상수 10은 휘발성이고 곧 없어지므로 이를 참조하는 것은 의미가 없다. 대신 참조 자체를 상수로 만들어 가리키는 값을 수정할 수 없게 한다. 이제 rvalue를 수정하는 문제가 해결되었다. 이것은 기술적인 제한이 아니라 어리석은 문제를 피하기 위해 언어 명세로 정해진 것이다.
이것은 함수에 상수 참조를 통해 값을 받을 수 있게 해서, 임시 개체의 불필요한 복사와 생성을 방지하는 일반적인 C++ 관용구(idiom)을 가능하게 한다.
내부적으로 컴파일러는 리터럴 상수를 저장할 숨겨진 변수(lvalue)를 생성한 다음 해당 변수를 참조에 바인딩한다. 이는 위의 코드 예에서 우리가 수동으로 한것과 동일하다. 예를 들면 다음 코드와 같다.
// 이것은...
const int& ref = 10;
// ... 이렇게 변환된다.
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;
이제 참조는 스코프를 벗어날 때까지 실제 존재하는 것을 가리키며 값을 수정하는 것을 제외하곤 평소와 같이 사용할 수 있다.
const int& ref = 10;
std::cout << ref << "\n"; // OK!
std::cout << ++ref << "\n"; // error: increment of read-only reference 'ref'
결론
lvalue와 rvalue의 의미를 이해함으로써 C++ 내부 작동 방식을 이해할 수 있었다. C++11은 rvalue 참조와 move semantics개념을 도입하여서 rvalue도 수정할 수 있게 되었고 rvalue의 한계를 확장했다. 이에 대해서는 다음 글에서 좀 더 자세히 살펴볼 예정이다.
관련 링크
https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c
Understanding the meaning of lvalues and rvalues in C++
A lightweight introduction to a couple of basic C++ features that act as a foundation for bigger structures.
www.internalpointers.com