다음을 통해 공유


값 범주 및 해당 참조

이 항목에서는 C++에 존재하는 다양한 범주의 값(및 값 참조)을 소개하고 설명합니다.

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

분명히 lvaluesrvalues에 대해 들어봤을 것입니다. 그러나 이 항목이 제시하는 용어로는 생각하지 않을 수도 있습니다.

C++의 모든 식은 위에 나열된 다섯 가지 범주 중 하나에 속하는 값을 생성합니다. C++ 언어에는 이러한 값 범주와 참조에 대한 적절한 이해가 필요한 기능과 규칙이 있습니다. 이러한 측면에는 값 주소 가져오기, 값 복사, 값 이동, 다른 함수로 값 전달 등이 포함됩니다. 이 토픽에서는 이러한 내용을 심층적으로 다루지는 않을 것이며, 확실한 이해를 돕기 위한 기본 정보를 제공합니다.

이 토픽의 정보는 Stroustrup의 두 가지 독립 속성 ID 및 이동성에 따른 값 범주 분석[2013 Stroustrup]을 기반으로 합니다.

ID를 갖고 있는 lvalue

값에 ID가 있다는 것은 무슨 의미일까요? 값의 메모리 주소를 갖고 있고(또는 가져올 수 있고) 안전하게 사용할 수 있다면 그 값이 바로 ID입니다. 이와 같은 방식으로 값의 콘텐츠를 비교하는 것보다 많은 것을 할 수 있습니다. 즉, ID로 비교하거나 구분할 수 있습니다.

lvalue에는 ID가 있습니다. 이제는 과거의 산물이 된 "lvalue"의 "l"는 "left"의 약어입니다(할당의 왼쪽처럼). C++에서 lvalue는 할당의 왼쪽 또는 오른쪽에 나타날 수 있습니다. "lvalue"의 "l"는 lvalue를 이해하거나 정의하는 데 실질적인 도움을 주지 못합니다. lvalue란 ID가 있는 값을 말한다는 것만 이해하면 됩니다.

lvalue인 식의 예로는 명명된 변수나 상수 또는 참조를 반환하는 함수가 있습니다. lvalue가 아닌 식의 예로는 임시 항목 또는 값이 반환하는 함수가 있습니다.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

이제 true 문에서 lvalue가 ID를 갖지만, xvalue도 마찬가지입니다. xvalue가 정확히 무엇인지 이 토픽의 뒷부분에서 살펴보겠습니다. 지금은 glvalue("일반화된 lvalue"용)라는 값 범주가 있다는 사실만 유의하세요. glvalue 세트는 lvalue(클래식 lvalue라고도 함)와 xvalue의 상위 집합입니다. 따라서 "lvalue에 ID가 있다"는 말은 true이지만, ID가 있는 부분의 전체 세트는 다음 일러스트레이션처럼 glvalue 세트입니다.

ID를 갖고 있는 lvalue

rvalue는 이동할 수 있지만, lvalue는 그렇지 않습니다.

하지만 glvalue가 아닌 값도 있습니다. 즉, 메모리 주소를 가져올 수 없는(또는 메모리 주소가 유효하다고 신뢰할 수 없는) 값이 있습니다. 위의 코드 예제에서 이러한 값을 목격했습니다.

신뢰할 수 있는 메모리 주소가 없는 것은 단점처럼 들립니다. 하지만 사실 이러한 값은 복사(일반적으로 많은 비용 발생)하는 대신 이동(일반적으로 저렴)할 수 있다는 장점이 있습니다. 값을 이동한다는 것은 값이 더 이상 이전 위치에 있지 않다는 것을 의미합니다. 따라서 이전에 있었던 위치에서 값에 액세스하려고 하면 안 됩니다. 값을 이동하는 시기 및 방법은 이 토픽의 범위를 벗어납니다. 이 토픽에서는 이동 가능한 값이 rvalue(또는 클래식 rvalue)라고 알려져 있다는 것만 알면 됩니다.

"rvalue"의 "r"은 "오른쪽"의 약어입니다(할당의 오른쪽처럼). 하지만 rvalue 및 rvalue 참조를 할당 외부에 사용할 수도 있습니다. "rvalue"의 "r"은 중요하지 않습니다. 우리가 rvalue라고 부르는 값이 이동 가능하다는 사실만 이해하면 됩니다.

반면 이 일러스트레이션처럼 lvalue는 이동할 수 없습니다. lvalue가 이동한다면 lvalue의 정의와 모순됩니다. 그리고 lvalue에 계속 액세스 할 수 있을 것으로 매우 합리적으로 예상되는 코드에 대한 예기치 않은 문제가 될 것입니다.

rvalue는 이동할 수 있지만, lvalue는 그렇지 않습니다.

따라서 lvalue를 이동할 수 없습니다. 하지만 이동할 수 있는 glvalue(ID가 있는 것들의 세트)라는 값이 있습니다. 값을 이동한 후에는 액세스하지 않도록 주의해야 한다는 점을 포함하여 충분한 이해가 있다면 사용할 수 있는 xvalue도 있습니다. 이 토픽의 뒷부분에서 값 범주의 전체 그림을 살펴볼 때 이 아이디어를 다시 한 번 검토하겠습니다.

Rvalue 참조 및 참조 바인딩 규칙

이 섹션에서는 rvalue에 대한 참조의 구문을 소개합니다. 다른 토픽에서 값의 이동과 전달에 대해 자세히 알아보겠지만, rvalue 참조는 그러한 문제 해결의 필수적인 부분이라고만 말해두면 충분합니다. rvalue 참조를 살펴보기 전에, 우리가 이전에 "참조"라고 부르던 T&에 대해 정확하게 이해해야 합니다. 참조 사용자가 쓸 수 있는 값을 참조하는 "lvalue(비 const) 참조"입니다.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

lvalue 참조는 lvalue에 바인딩할 수 있지만 rvalue에는 바인딩할 수 없습니다.

그리고 참조의 사용자가 쓸 수 없는 개체(예: 상수)를 의미하는 lvalue const 참조(T const&)가 있습니다.

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

lvalue const 참조는 lvalue 또는 rvalue에 바인딩할 수 있습니다.

T 형식의 rvalue에 대한 참조 구문은 T&&로 작성됩니다. rvalue 참조는 이동 가능한 값, 다시 말해서 사용한 후 콘텐츠를 보존할 필요가 없는 값(예: 임시 항목)을 나타냅니다. 전체 지점이 rvalue 참조에 바인딩된 값으로부터 이동되므로(따라서 수정되므로) constvolatile 한정자(cv 한정자라고도 함)가 rvalue 참조를 적용하지 않습니다.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

rvalue 참조는 rvalue에 바인딩됩니다. 사실, 오버로드 확인 때문에 rvalue는 lvalue const 참조보다 rvalue 참조에 바인딩하는 것을 선호합니다. 하지만 앞서 언급했듯이 rvalue 참조는 lvalue에 바인딩할 수 없으므로, rvalue 참조는 콘텐츠를 보존할 필요가 없는 것으로 가정하는 값(즉, 이동 생성자의 매개 변수)을 의미합니다.

또한 복사 생성을 통해(또는 rvalue가 xvalue인 경우 이동 생성을 통해) by-value 인수가 필요한 곳에 rvalue를 전달할 수 있습니다.

ID가 있는 glvalue, ID가 없는 prvalue

이제 ID가 무엇인지 다들 이해하셨을 것입니다. 이동 가능한 값과 이동할 수 없는 값도 이해하셨을 것입니다. 하지만 ID가 없는 값 세트의 이름을 지정하지 않았습니다. 이 세트를 prvalue 또는 순수 rvalue라고 합니다.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

ID가 있는 glvalue, ID가 없는 prvalue

값 범주의 전체 그림

위의 정보와 일러스트레이션을 하나의 큰 그림으로 결합하면 다음과 같은 그림이 완성됩니다.

값 범주의 전체 그림

glvalue(i)

glvalue(일반화된 lvalue)는 ID가 있습니다. "ID가 있습니다"의 약식으로 "i"를 사용합니다.

lvalue (i&!m)

lvalue(glvalue의 일종)는 ID가 있지만 이동할 수 없습니다. 참조나 const 참조, 또는 복사하는 것이 저렴한 경우 값을 통해 전달하는 일반적인 읽기-쓰기 값입니다. lvalue는 rvalue 참조에 바인딩할 수 없습니다.

xvalue(i&m)

xvalue(glvalue의 일종이지만 동시에 rvalue의 일종이기도 함)는 ID가 있고 이동할 수 있습니다. 복사는 비용이 많이 들기 때문에 이동하기로 결정한 이전 lvalue일 수 있으며, 이동 후에 액세스하지 않도록 주의해야 합니다. lvalue를 xvalue로 변환하는 방법은 다음과 같습니다.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

위의 코드 예제에서는 아직 아무 것도 이동하지 않았습니다. lvalue를 이름이 지정되지 않은 rvalue 참조로 캐스팅하여 xvalue를 만들었을 뿐입니다. 여전히 lvalue 이름으로 식별할 수 있지만, 이제는 xvalue로써 이동이 가능합니다. 이동하는 이유와 이동의 결과는 다른 토픽에서 다루겠습니다. 하지만 도움이 된다면 "xvalue"의 "x"가 "expert-only(전문가 전용)"를 의미한다고 생각하셔도 좋습니다. lvalue를 xvalue(rvalue의 일종)로 캐스팅하면 값을 rvalue 참조에 바인딩할 수 있게 됩니다.

다음은 명명되지 않은 rvalue 참조를 반환하는 함수를 호출한 후 xvalue 멤버에 액세스하는 두 가지 xvalue 예제입니다.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

prvalue(순수 rvalue. rvalue의 일종)는 ID가 없지만 이동할 수 있습니다. 이는 일반적으로 임시이거나, 값에 따라 반환하는 함수를 호출한 결과이거나, glvalue가 아닌 다른 식을 평가한 결과입니다.

rvalue(m)

rvalue는 이동할 수 있습니다. "m"은 "이동 가능"의 약식으로 사용합니다.

rvalue 참조는 항상 rvalue(콘텐츠를 보존할 필요가 없는 것으로 가정하는 값)을 참조합니다.

그렇다면 rvalue 참조 자체는 rvalue일까요? 명명되지 않은 rvalue 참조(위의 xvalue 코드 예제에 나온 것처럼)는 xvalue이므로 rvalue입니다. 이동 생성자와 마찬가지로 rvalue 참조 함수 매개 변수에 바인딩하는 것을 선호합니다. 반대로(그리고 반직관적으로) rvalue 참조의 이름이 있는 경우 해당 이름으로 구성된 식은 lvalue입니다. 따라서 rvalue 참조 매개 변수에 바인딩할 수 없습니다. 하지만 간단하게 바인딩하는 방법이 있습니다. 명명되지 않은 rvalue 참조(xvalue)에 다시 캐스팅하면 됩니다.

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

ID가 없고 이동할 수 없는 값은 아직 살피지 않았습니다. 하지만 이러한 값 범주는 C++ 언어에서 유용하지 않으므로 무시해도 됩니다.

참조 축소 규칙

한 식의 여러 유사 참조(lvalue 참조에 대한 lvalue 참조 또는 rvalue 참조에 대한 rvalue 참조)는 서로를 취소합니다.

  • A& &A&로 축소됩니다.
  • A&& &&A&&로 축소됩니다.

한 식의 여러 유사하지 않은 참조는 lvalue 참조로 축소됩니다.

  • A& &&A&로 축소됩니다.
  • A&& &A&로 축소됩니다.

전달 참조

마지막 섹션에서는 앞에서 살펴본 rvalue 참조를 전달 참조라는 다른 개념과 비교해 보겠습니다. "전달 참조"라는 용어가 만들어지기 전에 일부 사람들은 "범용 참조"라는 용어를 사용했습니다.

void foo(A&& a) { ... }
  • A&&는 앞서 살펴본 것처럼 rvalue 참조입니다. const 및 volatile은 rvalue 참조에 적용되지 않습니다.
  • fooA 형식의 rvalue만 수락합니다.
  • rvalue 참조(예: A&&)가 있는 이유는 임시 항목(또는 다른 rvalue) 전달에 최적화된 오버로드를 작성하기 위함입니다.
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&&전달 참조입니다. bar에 전달하는 항목에 따라 _Ty 형식은 휘발성/비휘발성 여부에 관계 없이 const/비 const일 수 있습니다.
  • bar_Ty 형식의 lvalue 또는 rvalue를 모두 수락합니다.
  • lvalue를 전달하면 전달 참조는 lvalue 참조 _Ty&로 축소되는 _Ty& &&가 됩니다.
  • lvalue를 전달하면 전달 참조는 lvalue 참조 _Ty&&가 됩니다.
  • 전달 참조(예: _Ty&&)가 있는 이유는 최적화가 아니라, 전달 참조에 항목을 투명하고 효율적으로 전달하는 것입니다. 라이브러리 코드를 작성(또는 면밀하게 연구)하는 경우에만 전달 참조를 경험할 가능성이 있습니다. 생성자 인수에 전달하는 팩터리 함수를 예로 들 수 있습니다.

소스

  • [Stroustrup, 2013] B. Stroustrup: C++ 프로그래밍 언어 버전 4. Addison-Wesley. 2013년.