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