[번역] Universal References in C++11 — Scott Meyers

원문: Scott Meyers, Universal References in C++11,
https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

번역: ChatGPT-3
수정: lunchballer


T&&가 항상 “Rvalue 참조”를 의미하는 것은 아닙니다.
by Scott Meyers

C++11에서 가장 중요한 새로운 기능 중 하나는 rvalue 참조입니다. 이것은 move semantics과 perfect forwarding이 구축되는 기초입니다. (rvalue 참조, move semantics, 또는 perfect forwarding의 기본 사항을 알지 못하면, 계속하기 전에 Thomas Becker의 개요를 읽으시기를 권합니다.)

구문적으로, rvalue 참조는 “보통” 참조(이제는 lvalue 참조로 알려져 있음)와 같은 방식으로 선언됩니다. 단지 하나 대신에 두 개의 앰퍼샌드를 사용합니다. 이 함수는 rvalue-reference-to-Widget 타입의 매개변수를 취합니다:

void f(Widget&& param);

rvalue 참조는 “&&”을 사용하여 선언되므로, 유형 선언에서 “&&”의 존재는 rvalue 참조를 나타내는 것으로 추정할 수 있습니다. 그러나 그렇지 않습니다.

Widget&& var1 = someWidget;      // here, “&&” means rvalue 참조
 
auto&& var2 = var1;              // here, “&&” does not mean rvalue 참조
 
template<typename T>
void f(std::vector<T>&& param);  // here, “&&” means rvalue 참조
 
template<typename T>
void f(T&& param);               // here, “&&”does not mean rvalue 참조

본 글에서는 유형 선언에서 “&&”의 두 가지 의미를 설명하고, 이들을 구분하는 방법을 설명하며, “&&”의 의미가 의도한 바를 명확하게 전달할 수 있는 새 용어를 소개합니다. 이러한 차이점을 구별하는 것은 중요합니다. 왜냐하면 유형 선언에서 “&&”를 볼 때마다 “rvalue 참조”라고 생각한다면, 많은 C++11 코드를 잘못 읽을 수 있기 때문입니다.

이 문제의 본질은 유형 선언에서 “&&”가 때로는 rvalue 참조를 의미하지만, 때로는 rvalue 참조나 lvalue 참조를 모두 의미한다는 것입니다. 이러한 이유로 소스 코드에서 “&&”의 일부는 실제로 “&&”의 구문적 모습을 가지고 있지만, lvalue 참조(“&”)의 의미를 가질 수 있습니다.[1] 이러한 참조는 lvalue 참조나 rvalue 참조보다 더 유연합니다. 예를 들어, rvalue 참조는 rvalue에만 바인딩될 수 있으며, lvalue 참조는 lvalue에 바인딩될 수 있을 뿐만 아니라 제한된 상황에서만 rvalue에도 바인딩될 수 있습니다. 반면 “&&”로 선언된 참조는 lvalue 참조나 rvalue 참조가 될 수 있으므로 어떤 것에도 바인딩될 수 있습니다. 이러한 비정상적으로 유연한 참조에는 고유한 이름이 필요합니다. 저는 이를 universal 참조라고 부릅니다.

“&&”가 universal 참조를 나타내는 경우(즉, 소스 코드에서 “&&”가 실제로 “&”를 의미할 수 있는 경우)의 세부 사항은 복잡하므로, 세부 사항은 나중에 다룰 예정입니다. 현재는 일상적인 프로그래밍에서 기억해야 할 다음의 규칙에 초점을 맞추어 봅시다:

“만약 어떤 유추된 타입 T에 대해 변수나 매개변수가 T&& 타입으로 선언되었다면, 해당 변수나 매개변수는 universal 참조입니다.”

유형 추론이 필요한 요건일 때는 universal 참조가 발견될 수 있는 상황이 될 수 있습니다. 실제로 대부분의 universal 참조는 함수 템플릿의 매개변수에 있습니다. auto로 선언된 변수의 유형 추론 규칙은 실질적으로 템플릿과 동일하기 때문에 auto로 선언된 universal 참조도 가능합니다. 이들은 제작 코드에서 흔하지는 않지만 예시에서는 템플릿보다 덜 복잡합니다. 이 글의 “세세한 사항” 섹션에서는 typedef 및 decltype의 사용과 관련하여 universal 참조가 발생할 수 있다는 것도 설명하겠지만, 세부적인 내용을 다루기 전에 universal 참조가 함수 템플릿 매개변수와 auto로 선언된 변수에만 해당된다고 가정합니다.

universal 참조의 형태가 T&&여야 하는 제약 조건은 보이는 것보다 더 중요하지만, 나중에 조금 더 자세히 살펴보겠습니다. 지금은 단순히 해당 요구 사항을 기억해 두시기 바랍니다.

모든 참조와 마찬가지로, universal 참조는 초기화되어야 하며, 이를 통해 해당 레퍼런스가 lvalue 참조인지 rvalue 참조인지를 결정합니다.

  • 만약 universal 참조를 초기화하는 표현식이 lvalue라면, universal 참조는 lvalue 참조가 됩니다.
  • 만약 universal 참조를 초기화하는 표현식이 rvalue라면, universal 참조는 rvalue 참조가 됩니다.

이 정보는 lvalue와 rvalue를 구별할 수 있는 경우에만 유용합니다. 이 용어들의 정확한 정의를 계속 설명하기 어렵습니다 (C++11 표준은 일반적으로 특정 경우에 표현식이 lvalue인지 rvalue인지 지정합니다). 그러나 실제로는 다음 것들로 충분합니다:

  • 표현식의 주소를 가져올 수 있다면, 해당 표현식은 lvalue입니다.
  • 표현식의 유형이 lvalue 참조(T& 또는 const T& 등)인 경우 해당 표현식은 lvalue입니다.
  • 그렇지 않으면 표현식은 rvalue입니다. 개념적으로(보통 사실상도 그렇지만), rvalue는 함수에서 반환되거나 암시적 유형 변환을 통해 생성된 임시 객체와 같은 것을 나타냅니다. 대부분의 리터럴 값(예: 10과 5.3)도 rvalue입니다.

본 글의 초반에 등장한 다음 코드를 다시 생각해 봅시다.

Widget&& var1 = someWidget;
auto&& var2 = var1;

var1의 주소를 가져올 수 있으므로 var1은 lvalue입니다. var2의 타입 선언인 auto&&는 유형 선언으로서 universal 참조입니다. 그리고 var1(하나의 lvalue)로 초기화되고 있기 때문에, var2는 lvalue 참조가 됩니다. 이러한 코드를 대충 읽으면, “&&”이 선언되어 있으므로 var2는 rvalue 참조일 것이라고 생각할 수 있습니다. 그러나 var2가 초기화되는 유니버설 레퍼런스는 lvalue이므로, var2는 lvalue 참조가 됩니다. 이것은 마치 var2가 다음과 같이 선언된 것처럼 보입니다.

Widget& var2 = var1;

위에서 언급한 대로, 표현식이 lvalue 참조 타입이면 lvalue입니다. 예시를 살펴보면:

std::vector<int> v;
...
auto&& val = v[0];  // val becomes an lvalue 참조(see below)

val은 universal 참조이며, v[0]으로 초기화됩니다. 즉, std::vector::operator[] 함수의 결과인 lvalue 참조를 반환합니다. 이는 모든 lvalue 참조가 lvalue이기 때문에 val은 lvalue 참조가 되며, 이 lvalue가 val의 초기값으로 사용되기 때문에, val은 rvalue 참조로 선언되었음에도 불구하고 lvalue 참조가 됩니다.

Universal 참조는 보통 템플릿 함수의 매개변수로 사용되는 것이 가장 일반적이라는 것을 언급했습니다.

이전 글에서 다룬 템플릿 코드를 다시 살펴보면:

template<typename T>
void f(T&& param);               // "&&"는 rvalue 참조일 수도 있다

다음과 같이 f를 호출한다면:

f(10);                           // 10은 rvalue

param은 리터럴 10으로 초기화 되고, 이는 주소를 가져올 수 없으므로 rvalue가 된다. 이러한 이유로 f 함수 호출 시 universal 참조인 param은 rvalue로 초기화되어 int&& 타입이 됩니다.

반면, f 함수가 다음과 같이 호출되면,

int x = 10;
f(x);           // x is an lvalue

param은 변수 x로 초기화되며, 주소를 가져올 수 있으므로 lvalue입니다. 따라서 이 호출에서 universal 참조인 param은 lvalue로 초기화되며, int&가 됩니다.

f 함수 선언 옆에 있는 주석이 이제 명확해야 합니다. param의 유형이 lvalue 참조인지 rvalue 참조인지는 f가 호출될 때 전달되는 것에 달려 있습니다. 때로는 param이 lvalue 참조가 되고, 때로는 rvalue 참조가 됩니다. param은 정말로 universal 참조입니다.

“&&”는 유형 유도(type deduction)가 이루어지는 경우에만 universal 참조를 나타냅니다. 유형 유추가 없는 경우, 유형 선언에서 “&&”는 항상 rvalue 참조를 의미합니다.

따라서:

template<typename T>
void f(T&& param);               // deduced parameter type ⇒ type deduction;
                                 // && ≡ universal reference
 
template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);        // fully specified parameter type ⇒ no type deduction;
    ...                          // && ≡ rvalue reference
};
 
template<typename T1>
class Gadget {
    ...
    template<typename T2>
    Gadget(T2&& rhs);            // deduced parameter type ⇒ type deduction;
    ...                          // && ≡ universal reference
};
 
void f(Widget&& param);          // fully specified parameter type ⇒ no type deduction;
                                 // && ≡ rvalue reference

이러한 예제들에 대해서는 놀라운 것이 없습니다. 여기서 T&& (여기서 T는 템플릿 매개변수입니다)를 보면 형식 유추가 있으므로 universal 참조로 볼 수 있습니다. 또한 특정 형식 이름 뒤에 “&&”을 본다면 (예: Widget&&), rvalue 참조를 볼 수 있습니다.

제가 말한 것처럼, 참조 선언의 형식은 “T&&” 여야만 참조가 universal 참조가 되도록 할 수 있습니다. 이는 중요한 제한 사항입니다. 이 글의 시작 부분에서 다음과 같이 선언한 것을 다시 살펴봅시다.

template<typename T>
void f(std::vector<T>&& param); // "&&"은 rvalue 참조임을 의미

여기에서는 형식 추론과 “&&”으로 선언된 함수 매개변수가 모두 존재하지만, 매개변수 선언의 형식이 “T&&”이 아니라 “std::vector&&”입니다. 따라서 매개변수는 일반적인 rvalue 참조이며, universal 참조가 아닙니다. Universal 참조는 반드시 “T&&” 형식으로만 나타날 수 있습니다! 단순히 const 한정자를 추가하는 것만으로도 “&&”를 universal 참조로 해석하는 것을 방지할 수 있습니다.

template<typename T>
void f(const T&& param);    // "&&"은 rvalue 참조임을 의미

이제 “T&&”는 universal 참조를 나타내는 데 필요한 형식일 뿐입니다. 템플릿 매개변수의 이름으로 T를 사용할 필요는 없습니다.

template<typename MyTemplateParamType>
void f(MyTemplateParamType&& param); // "&&"는 universal 참조를 의미함

때로는 T가 템플릿 매개변수인 함수 템플릿 선언에서 T&&을 볼 수 있지만, 형식 추론이 아직 이루어지지 않은 경우가 있습니다.

아래 push_back 함수를 보면,

template <class T, class Allocator = allocator<T> >
class vector 
{
public:
    ...    
		void push_back(T&& x);  //fully specified parameter type ⇒ no type deduction;    
		...                     // && ≡ rvalue 참조
};

위의 코드에서, T는 템플릿 파라미터이고 push_back 함수는 T&&를 인자로 받지만, 해당 인자는 universal 참조가 아닙니다. 이게 어떻게 가능할까요?

이 질문에 대한 답은 push_back이 클래스 바깥에서 어떻게 선언되는지를 보면 알 수 있습니다. std::vector의 Allocator 파라미터가 불필요하고 코드만 더러워지므로, 우리는 이에 대한 언급을 생략하겠습니다. 따라서, std::vector::push_back의 이 버전에 대한 선언은 다음과 같습니다:

template <class T>
void vector<T>::push_back(T&& x);

push_back은 자체로 존재할 수 없습니다. 하지만 우리가 std::vector를 가지고 있다면, 이미 T가 무엇인지 알고 있으므로 T를 추론할 필요가 없습니다.

예시를 들면 이해하기 쉬울 것입니다. 다음과 같이 코드를 작성한다면,

Widget makeWidget();         // Widget을 생성하는 팩토리 함수
std::vector<Widget> vw;
...
Widget w;
vw.push_back(makeWidget());  // 팩토리 함수로 Widget을 생성하고, vw에 추가

이렇게 push_back을 사용하면 컴파일러는 std::vector 클래스를 위해 그 함수를 인스턴스화합니다. 그리고 push_back의 선언은 다음과 같습니다.

void std::vector<Widget>::push_back(Widget&& x);

이것을 보면, 클래스가 std::vector임을 알면 push_back의 매개변수 유형이 완전히 결정된다는 것을 알 수 있습니다. 즉, 매개변수 유형은 Widget&&입니다. 이 경우 타입 추론이 필요하지 않습니다.

emplace_back 함수의 경우, 다음과 같이 선언됩니다.

template <class T, class Allocator = allocator<T> >
class vector {
public:
...
template <class... Args>
void emplace_back(Args&&... args);        // 유추된 매개변수 타입 ⇒ 타입 추론
...                                       // && ≡ universal 참조
};

가변인자를 가지고 있는 것을 제외하고는, emplace_back이 각 인자의 타입을 유추해야 한다는 사실을 간과해서는 안됩니다. 함수 템플릿 매개변수인 Args는 클래스 템플릿 매개변수 T와 독립적이므로, std::vector과 같은 클래스임을 알았다고 해서 emplace_back이 받는 인자의 타입이 무엇인지 알 수 없습니다. std::vector에 대한 emplace_back의 외부 선언은 이를 명확하게 해줍니다 (Allocator 매개변수는 여전히 무시합니다).

template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);

따라서 클래스가 std::vector<Widget>임을 알았다고 하더라도, 컴파일러는 여전히 emplace_back에 전달되는 타입을 유추해야 합니다. 결과적으로, std::vector::emplace_back의 매개변수는 rvalue 참조가 아닌 universal 참조입니다.

마지막으로 기억할 가치가 있는 한 가지 점은, 표현식의 lvalue나 rvalue 여부는 그 타입과 독립적이라는 것입니다. int 타입을 살펴보겠습니다. int 타입은 lvalue(예: int로 선언된 변수)와 rvalue(예: 10과 같은 리터럴)가 있습니다. Widget과 같은 사용자 정의 타입도 마찬가지입니다. Widget 객체는 lvalue(예: Widget 변수) 또는 rvalue(예: Widget을 만드는 팩토리 함수에서 반환된 객체)일 수 있습니다. 표현식의 타입은 그것이 lvalue인지 rvalue인지를 알려주지 않습니다.

표현식의 lvalueness 또는 rvalueness는 해당 유형과 독립적이므로, rvalue 참조 유형인 lvalue가 존재하거나 rvalue 참조 유형인 rvalue가 존재할 수 있습니다.

Widget makeWidget();                       // factory function for Widget
 
Widget&& var1 = makeWidget()               // var1 is an lvalue, but
                                           // its type is rvalue reference (to Widget)
 
Widget var2 = static_cast<Widget&&>(var1); // the cast expression yields an rvalue, but
                                           // its type is rvalue reference  (to Widget)

lvalue (예: var1)을 rvalue로 변환하는 전통적인 방법은 이를 std::move로 사용하는 것입니다. 따라서 var2는 다음과 같이 정의될 수 있습니다.

Widget var2 = std::move(var1); // 위와 동등한 코드

static_cast로 보여준 것은 표현식의 타입이 rvalue 참조(Widget&&)임을 명시하기 위함입니다.

rvalue 참조 타입의 명명된 변수와 매개변수는 lvalue입니다. (그들의 주소를 가져올 수 있습니다.) 이전에 보았던 Widget과 Gadget 템플릿을 다시 생각해보세요.

template<typename T>
class Widget {
...
Widget(Widget&& rhs); // rhs의 타입은 rvalue 참조이지만, rhs 자체는 lvalue입니다.
...
};

template<typename T1>
class Gadget {
...
template <typename T2>
Gadget(T2&& rhs); // rhs는 기본적으로 rvalue 참조 또는 lvalue 참조로 최종적으로 변환될 universal 참조입니다.  하지만 rhs 자체는 lvalue입니다.

... 
};

Widget의 생성자에서 rhs는 rvalue 참조 타입이므로, rhs가 rvalue에 바인딩되었다는 것을 알 수 있습니다. 그러나 rhs 자체는 lvalue이므로, 그것이 바인딩된 대상의 rvalueness를 활용하려면 rhs를 다시 rvalue로 변환해야합니다. 이를 위해 일반적으로 우리는 move를 사용하여 lvalue를 rvalue로 변환합니다. 마찬가지로 Gadget 생성자에서 rhs는 universal 참조이므로 lvalue나 rvalue에 바인딩될 수 있지만, 무엇에 바인딩되었는지와 관계없이 rhs 자체는 lvalue입니다. 만약 rhs가 rvalue에 바인딩되어 있고 그것의 rvalueness를 활용하고 싶다면, rhs를 rvalue로 다시 변환해야합니다. 만약 lhs가 lvalue에 바인딩되어 있다면, 물론 우리는 그것을 rvalue처럼 다루고 싶지 않습니다. Universal 참조가 바인딩된 대상의 lvalueness와 rvalueness의 모호성은 std::forward의 동기가 되며, 이 함수는 universal 참조 lvalue를 가져와 바인딩된 식이 rvalue인 경우에만 rvalue로 변환합니다. 이러한 변환을 수행하고자 하는 우리의 목적은 대개 호출 인수의 lvalueness나 rvalueness를 보존하고(즉, forwarding) 다른 함수로 전달하기 위함입니다.

하지만 std::move와 std::forward는 이 글의 초점이 아닙니다. “&&”가 유형 선언에서 rvalue 참조를 선언할 수도, 그렇지 않을 수도 있는 사실이 이 글의 초점입니다. 이에 따라, std::move와 std::forward에 대한 정보는 Further Information 섹션의 참조 자료를 참고하시기 바랍니다.

세세한 사항

문제의 본질은 C++11에서 일부 구조가 참조에 대한 참조를 만든다는 것입니다. 참조에 대한 참조는 C++에서 허용되지 않습니다. 만약 소스 코드에 참조에 대한 참조가 명시적으로 포함되어 있으면 코드는 유효하지 않습니다:

Widget w1;
...
Widget& & w2 = w1; // 오류! "참조에 대한 참조"란 것은 존재하지 않습니다.

그러나 일부 경우에는 컴파일 중에 발생하는 유형 조작으로 인해 참조에 대한 참조가 발생하며, 이러한 경우 코드를 거부하는 것이 문제가 될 수 있습니다. 이는 C++98/C++03의 초기 표준에서 경험했던 것으로 알려져 있습니다.

템플릿 매개변수로 universal 참조가 있는 경우 타입 추론 중에, 같은 타입의 lvalue와 rvalue는 조금 다른 타입으로 추론됩니다. 특히, 타입 T의 lvalue는 T& (즉, T에 대한 lvalue 참조) 타입으로 추론되고, 타입 T의 rvalue는 단순히 T 타입으로 추론됩니다. (lvalue는 lvalue 참조로 추론되지만, rvalue는 rvalue 참조로 추론되지 않는다는 것을 주목하세요!) Universal 참조를 사용하는 템플릿 함수를 lvalue와 rvalue로 호출하는 경우를 생각해보세요:

template<typename T>
void f(T&& param);

...

int x;

...

f(10); // rvalue로 f 호출
f(x); // lvalue로 f 호출

rvalue 10으로 호출된 f의 T는 int로 추론되며, 인스턴스화된 f는 다음과 같이 보입니다:

f(int&& param); // rvalue에서 인스턴스화된 f

좋습니다. 그러나 lvalue x로 f를 호출하는 경우, T는 int&로 추론되며, f의 인스턴스화에는 레퍼런스를 가지는 레퍼런스가 포함됩니다:

void f(int& && param); // lvalue에서 초기 인스턴스화된 f

참조에 대한 참조 때문에, 이 인스턴스화된 코드는 원칙적으로 유효하지 않지만, “f(x)”라는 소스 코드는 완전히 합리적입니다. C++11은 이러한 상황에서 참조에 대한 참조가 발생하면 “레퍼런스 축소”를 수행하여 거부하지 않도록 합니다.

참조에는 lvalue 참조와 rvalue 참조 두 종류가 있기 때문에, 참조에 대한 참조 조합은 네 가지가 가능합니다: lvalue 참조에서 lvalue 참조, lvalue 참조에서 rvalue 참조, rvalue 참조에서 lvalue 참조, rvalue 참조에서 rvalue 참조. 따라서 참조에 대한 참조 조합에 따라 레퍼런스 축소규칙도 두 가지만 존재합니다:

  1. rvalue 참조에서 rvalue 참조로의 참조에 대한 참조 조합은 rvalue 참조로 축소됩니다.
  2. 나머지 모든 참조에 대한 참조 조합(즉, lvalue 참조가 포함된 모든 조합)은 lvalue 참조로 축소됩니다.

위 룰을 lvalue에 대한 f의 인스턴스화에 적용하면, 컴파일러가 호출을 처리하는 방식으로 다음 유효한 코드가 생성됩니다:

void f(int& param); // lvalue에 대한 f 인스턴스화 후 참조 축소

이는 universal 참조가 형식 추론 및 레퍼런스 축소 이후 lvalue 참조가 될 수 있는 정확한 메커니즘을 보여줍니다. 사실 universal 참조는 레퍼런스 축소 컨텍스트에서의 rvalue 참조일 뿐입니다.

변수 자체가 참조일 때 그 변수의 타입을 추론하는 경우 더 복잡해집니다. 이 경우, 타입의 참조 부분은 무시됩니다. 예를 들어

int x;

...

int&& r1 = 10; // r1의 타입은 int&&이다
int& r2 = x; // r2의 타입은 int&이다

f 템플릿을 호출할 때, r1과 r2의 타입은 모두 int로 간주됩니다. 이 참조 제거 동작은 universal 참조의 타입 추론 중 lvalue는 T& 타입으로 추론되고 rvalue는 T 타입으로 추론되는 규칙과 독립적입니다. 따라서, 다음과 같이 호출한다면,

f(r1);

f(r2);

r1과 r2의 추론된 타입은 모두 int&이 됩니다. 그 이유는 먼저 r1과 r2의 타입에서 참조 부분이 제거되어 int가 된 다음, 각각이 lvalue이므로 f 호출의 universal 참조 매개변수의 타입 추론 중 int&로 처리되기 때문이다.

참조 축소는 “템플릿 인스턴스화와 같은 문맥에서 발생”한다고 언급했습니다. 두 번째로는 auto 변수의 정의입니다. Universal 참조를 가진 auto 변수의 유형 추론은 실제로 universal 참조를 가진 함수 템플릿 매개변수의 유형 추론과 거의 동일합니다. 따라서 T 유형의 lvalue는 T& 유형으로 추론되고 T 유형의 rvalue는 T 유형으로 추론됩니다. 이 글의 시작 부분에서 다시 살펴보는 예제를 고려해 보겠습니다:

Widget&& var1 = someWidget; // var1의 유형은 Widget&& (여기에는 auto 사용 없음)

auto&& var2 = var1; // var2의 유형은 Widget& (아래 참조)

var1은 Widget&& 유형이지만, universal 참조의 유형 추론에서는 참조 부분이 무시됩니다. 따라서 Widget 유형으로 간주됩니다. universal 참조(var2)를 초기화하는 데 사용되는 lvalue이기 때문에, 해당 유형은 Widget&로 추론됩니다. var2의 정의에서 auto 대신 Widget&를 대체하면 다음의 잘못된 코드가 생성됩니다.

Widget& && var2 = var1;   // 참조에 대한 참조에 주의

이를 참조 축소한 결과는 다음과 같습니다.

Widget& var2 = var1; // var2의 유형은 Widget&입니다.

세 번째 참조 축소는 typedef 선언과 사용에서 일어납니다. 다음 클래스 템플릿을 고려해 보겠습니다.

template<typename T>
class Widget {
typedef T& LvalueRefType;
...
};

그리고 다음과 같이 템플릿을 사용하면,

Widget<int&> w;

인스턴스화된 클래스는 다음 (잘못된) typedef를 포함합니다.

typedef int& & LvalueRefType;

참조 축소를 적용하면 이것이 합법적인 코드로 축소됩니다.

typedef int& LvalueRefType;

그런 다음 참조가 적용되는 문맥에서 이 typedef를 사용하면 참조가 적용되는 상황에서 다음과 같은 잘못된 코드가 생성됩니다.

void f(Widget<int&>::LvalueRefType&& param);

 typedef 적용 이후에 다음과 같은 유효하지 않는 코드가 만들어진다.

void f(int& && param);

그러나 참조 축소가 발생하여 f의 최종 선언은 다음과 같습니다.

void f(int& param);

참조 붕괴가 일어나는 마지막 문맥은 decltype의 사용입니다. 템플릿과 auto와 마찬가지로 decltype은 T 또는 T& 타입을 반환하는 표현식에 대해 타입 추론을 수행하고, 그 결과에 C++11의 참조 붕괴 규칙을 적용합니다. 그러나 decltype이 사용하는 타입 추론 규칙은 템플릿 또는 auto의 타입 추론과 동일하지 않습니다. 이에 대한 자세한 내용은 여기서 다루지 않겠지만(Further Information 섹션에서 자세한 정보를 찾을 수 있음), 주목할 만한 차이점 중 하나는 non-reference 타입의 변수에 대해 decltype은 타입 T(즉, non-reference 타입)을 추론하는 반면, 템플릿과 auto는 동일한 조건에서 T& 타입을 추론한다는 것입니다. 또 다른 중요한 차이점은 decltype의 타입 추론이 초기화 표현식의 타입과는 무관하다는 것입니다. 따라서 다음과 같은 코드가 만들어집니다.

Widget w1, w2;

auto&& v1 = w1;         // v1은 lvalue로 초기화되는 auto 기반 범용 참조입니다. 따라서 v1은
                        // w1을 참조하는 lvalue 참조가 됩니다.

decltype(w1)&& v2 = w2; // v2는 decltype 기반 범용 참조이고 decltype(w1)은 Widget이므로
                        // v2는 rvalue 참조가 됩니다. w2는 lvalue이므로
                        // rvalue 참조로 lvalue를 초기화하는 것은 허용되지 않으므로
                        // 이 코드는 컴파일되지 않습니다.

Summary

타입 선언에서 ‘&&’는 rvalue 참조 또는 universal 참조를 나타냅니다. universal 참조는 lvalue 참조 또는 rvalue 참조 중 어느 쪽으로도 해석될 수 있는 레퍼런스입니다. Universal 참조는 항상 추론된 형식 T를 가진 T&&의 형태를 가집니다.

참조 축소는 참조에 대한 참조 상황이 컴파일 중에 발생할 수 있는 특정 상황에서 universal 참조(실제로는 레퍼런스 축소가 발생하는 상황에서 rvalue 참조)가 때로는 lvalue 참조로, 때로는 rvalue 참조로 해석되도록 하는 메커니즘입니다. 이는 템플릿 형식 추론, auto 형식 추론, typedef 형성 및 사용, 그리고 decltype 표현식이라는 지정된 문맥에서 발생합니다.

Leave a Reply

Your email address will not be published. Required fields are marked *