[C++] Value Category
Value category를 다시 한 번 보고 가야할 것 같았다. 다음의 정리 주제는 perfect forwarding 또는 Universal reference인데, 그 과정에서 rvalue reference, lvalue reference에 대한 이해를 좀 더 할 수 있을 것 같다.
소개
lvalue, rvalue? Value가 무엇을 의미하나요?
C++에서 lvalue와 rvalue 개념이 C에서보다 확장되었습니다.
우선 기존 C에서 사용했던 left value, right value 정의를 살펴보면 다음과 같이 되어있습니다.
-
lvalue : 대입 시에 왼쪽 혹은 오른쪽에 오는 식(expression)
-
rvalue : 대입 시에 오직 오른쪽에 오는 식(expression)
하지만 C++에서는 개념을 좀 더 확장하여 다음과 같이 정의합니다.
- lvalue : 식(expression)이 끝난 후 계속 존재하는 값
- rvalue : 식(expression)이 끝나면 존재하지 않는 값
C++에서 lvalue와 rvalue의 몇 가지 예시를 코드를 통해 알아봅시다.
int func() {
return 0;
}
int main() {
int n = 0, m = 4;
n; // lvalue
0; // rvalue
func(); // rvalue
n < m ? n : m; // n과 m이 lvalue이면 전체 표현식도 lvalue
}
lvalue와 rvalue는 C++ 참조 개념과 밀접한 연관을 갖고 있습니다.
이 글은 C++11에 추가된 rvalue reference를 비롯하여 전반적인 value에 대해 다룹니다.
lvalue 참조와 rvalue 참조
C++11에서는 lvalue reference(&) 이외에 rvalue reference(&&)라는 개념이 추가되었습니다.
value와 참조(reference)에 대해서 함께 이해가 필요합니다.
lvalue reference (&)
C++에서 참조형(&) 변수는 선언과 동시에 반드시 초기화가 이루어져야 하는 등 몇 가지 조건이 있습니다.
이렇게 &
를 사용한 참조 방식을 lvalue reference라고 부르며, lvalue reference에서 rvalue를 참조하려고 할 때는 에러가 발생합니다.
int main(){
int num = 0;
int& refNum = num; // OK!
int& refNum2 = 0; // Error!
int& refNum3 = ++num; // OK! '++num' is lvalue
int& refNum4 = num++; // Error! 'num++' is rvalue
return 0;
}
- lvalue reference(&)는 리터럴 상수 0(rvalue)을 참조할 수 없기 때문에 에러가 발생합니다.
- 대신 0은
const int
형이기 때문에 const int를 참조하는const int& refNum2 = 0
으로 사용할 수 있습니다.
- 대신 0은
- 전위 증감식은 lvalue 형태입니다. 값을 증감시킨 후 해당 값을 반환합니다.
- 후위 증감식은 rvalue 형태입니다. 값을 증감시키지만, 증감되기 전의 값을 임시로 보관하여 반환합니다. 반환되는 값은 임시로 존재하고 수식이 끝나는 순간 사라집니다.
rvalue reference (&&)
우측값 참조(rvalue reference)는 C++11에서 처음 소개된 기능으로 참조 기호는 &&
입니다.
&&
를 사용한 참조 방식을 rvalue reference라고 부르며, rvalue reference에서 lvalue를 참조하려고 할 때는 에러가 발생합니다.
int&& num = 0; // OK!
겉으로 보면 lvalue reference에 비해서 &
기호를 하나 더 사용한다는 것만 달라 보이지만, 실제 의미상으로도 lvalue reference와 rvalue reference는 차이가 있습니다.
int num = 0;
int& lvalue_ref_1 = num; // OK!
int& lvalue_ref_2 = 10; // Error! 10 is rvalue
int&& rvalue_ref_1 = 10; // OK!
int&& rvalue_ref_2 = num; // Error! num is lvalue
이렇게 lvalue reference는 lvalue를 참조해야 하고, rvalue는 rvalue를 참조해야 합니다.
C++ Value category
C++11부터 lvalue, rvalue 개념 뿐만이 아니라 prvalue, xvalue, glvalue라는 개념이 추가되었습니다.
C++11 표준에서는 이러한 value들을 다음과 같이 나누었습니다.
- has identity: it’s possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);
-
can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.
- has identity : 값이 식별성을 갖고 있는가? (glvalue)
- 공간의 성질을 갖고 있는 값의 통칭
- 데이터를 저장할 수 있는 메모리의 위치정보
- can be moved from : 값이 메모리에서 이동될 수 있는가? (rvalue)
- 자료의 성질을 갖고 있는 값의 통칭
- 저장될 데이터를 표현하는 비트열 리터럴
우선 기존의 lvalue가 갖는 특성을 identity를 갖는다고 표현하는데 C++11에서는 identity 이외에 move
특성이 추가되었습니다.
모든 값이 move
될 수 있는 것은 아니므로 C++에서는 move
될 수 있는지 없는지도 매우 중요한 특성이 되었습니다.
따라서 어떤 값이 move
될 수 있느냐 없느냐, identity를 갖을 수 있느냐 없느냐에 따라 4개의 조합이 되는데
이 중에서 move
될 수 없으면서 identity를 갖지 않는 값은 고려할 필요가 없으므로 3개의 조합이 남게 됩니다.
- lvalue :
identity
를 가지면서move
될 수 없는 표현식들 - xvalue :
identity
를 가지면서move
될 수 있는 표현식들 - prvalue :
identity
를 가지고있지 않으면서move
될 수 있는 표현식들 - glvalue :
identity
를 가지고있는 표현식들(lvalue, xvlaue모두 glvalue 표현식) - rvalue :
move
될 수 있는 표현식들(prvalue, xvalue 모두 rvalue 표현식) identity
를 가지고 있지 않으면서move
될 수 없는것들
C++11에서는 값이 식별성을 가지고 있는 경우 identity
를 갖는다고 표현하며, 값이 메모리에서 이동될 수 있는 것을 move
될 수 있다고 표현합니다.
(좀 더 쉬운 설명으로, 포인터로 가리킬 수 있는 값은 주소를 이동할 수 있으므로 move
될 수 있습니다.)
위의 경우에서 기본적인 분류단위를 Primary category
라고 하며 두 개 이상의 복합 분류를 Mixed category
라고 합니다.
Primary category
- lvalue
- prvalue
- xvalue
Mixed category
- glvalue(xvalue + lvalue)
- rvalue(xvalue + prvalue)
glvalue, identity
를 가지고 있는 표현식들(lvalue, xvalue 모두 glvalue 표현식)
glvalue는 xvalue 또는 lvalue를 뜻하며 공간의 성질을 갖고 있는 값들을 말합니다.
데이터를 저장할 수 있는 메모리의 위치정보를 가지고 있습니다.
glvalue는 lvalue에서 rvalue, 배열에서 포인터, 함수에서 포인터로 암시적 변환을 통해 prvalue로 변환될 수 있습니다.
rvalue, move
될 수 있는 표현식들(prvalue, xvalue 모두 rvalue 표현식)
rvalue는 메모리 주소에서 이동이 될 수 있는 표현식(xvalue, prvalue)을 의미합니다.
rvalue의 경우 주소를 얻을 수 없습니다. (아래와 같은 연산은 불가능)
int a = 1;
int* p = &a++; // error : 주소를 얻는 & 연산자에는 lvalue가 사용되어야 합니다.
// a++ 결과값 : rvalue
// ++a 결과값 : lvalue
이동(move) 가능한 값이란?
-
이동이란 어떤 공간의 데이터가 다른 공간으로 옮겨지는 작업
-
이동이 완료된 후에는, 원래 자리에 데이터가 남지 않음 (std::move의 매개변수로 전달된 데이터는 empty 상태가 된다.)
string str = "Hello World"; string s = std::move(str); // str is empty // s == "Hello World"
-
값의 성질을 갖고 있어야, 다른 공간으로 옮겨질 수 있음
-
공간성 정보는 다른 공간으로 옮겨질 수 없음
포인터 변수의 값은 공간의 정보를 값으로 수치화한 것이므로, 포인터 주소는 공간이 아니라 값에 해당함.
(단, 변수 이름 자체는 공간성 정보임에 유의)
lvalue, identity
를 가지면서 move
될 수 없는 값
모든 변수, 함수, 전위증감 표현식, lvalue 참조, 문자열 리터럴 등이 lvalue에 속합니다.
식별자를 갖지만 리터럴 등은 메모리상에서 이동될 수 없기 때문에 lvalue가 됩니다.
특징
- 대입문의 좌측에 올 수 있음
- &연산자로 값의 주소를 얻을 수 있고, 여기서 &는 참조자(reference)를 의미하지 않음
- 표현식이 끝나더라도 값이 살아있음
xvalue, identity
를 가지면서 move
될 수 있는 표현식들
identity
를 가지면서 move
될 수 있는 표현식들은 xvalue라고 합니다.
eXpiring에서 따와 xvalue라고 부르며, 말 그대로 만료되어가는 값을 뜻합니다.
그래서 표현식이 끝나고 표현식이 의미하는 주소로 접근했을 때 값이 존재할 수도 있고, 존재하지 않을 수도 있습니다.
어떤 데이터든 메모리 상에 저장되어야 하는데, prvalue가 메모리에 올라온 순간 주소를 갖게 되므로 prvalue가 아니게 됩니다.
컴파일러는 이러한 prvalue의 임시데이터를 저장할 공간이 필요한데, 이러한 데이터성 임시 객체를 xvalue라고 합니다.
예를들어 std::move(x)
와 같이 rvalue reference를 리턴할 수 있는 함수는 lvalue를 move하고, move한 값은 xvalue에 속하게 됩니다.
특징
- 컴파일러만 사용하므로 &연산자가 허용되지 않음
- 표현식이 끝나면 사라짐
- 표현식에서 rvalue가 주소를 갖는 임시 객체로 구체화된 형태
prvalue, identity
를 가지고 있지 않으면서 move
될 수 있는 표현식들
int a = 10;
int b = 20;
a++;
a + b;
if (a < b) { // a < b 의 결과값(true)은 prvalue
...
}
prvalue는 pure rvalue
의 약자로 후위 증감연산자, 문자열 리터럴을 제외한 모든 리터럴(ex: 42, true, nullptr) 등이 prvalue에 속합니다.
prvalue는 대입문 오른쪽에 올 수 있으며 주소가 없다는 특징을 가지고 있습니다.
특징
- 대입문의 우측에 올 수 있음
- 주소가 없음
참고 자료
- https://en.cppreference.com/w/cpp/language/value_category