본문으로 바로가기

C++11) auto 키워드

category C++/Modern 2019. 1. 30. 05:18

<기본 정리>


auto 키워드는 C++ 11에서 처음 등장했으며, 변수의 자료형을 컴파일 시간에 자동으로 추론해줍니다.

decltype 키워드와 같이 자주 쓰입니다.


기본 문법

auto temp;


 

<auto 사용 예제>

(잘못 작성했습니다. auto f = { 1 } 은 c++14 까진 std::initializer_list, c++17 부턴 int 입니다.)



auto를 사용함으로써, 긴 변수의 이름도 쉽게 작성할 수 있게 되었습니다.

예를 들어

<auto 키워드를 사용하기 전 vector를 순회하는 반복자의 선언>


<auto 키워드를 사용한 후 vector를 순회하는 반복자 선언>


이렇게 길고 복잡한 타입명을 a.begin의 리턴형에 맞춰 자동으로 추론해줍니다.




이런 경우는 어떨까요? template 함수 내에서 두 수를 곱하는 함수를 만들어 줬습니다.

그런데 int 형과 double 형을 곱하고 있군요, 이런 어쩌죠 abc에서는 T 값을 반환하니... int 형이 반환될텐데요 ?

이럴때도 auto 키워드를 사용하면 자동적으로 리턴 타입을 추론하여 뱉어줍니다.


< 이제 abc(a,b) 값은 double 형! >


물론 사용자 정의 타입에도 추론됩니다.



 <심화 내용>


1. auto의 추론 규칙

템플릿 에서의 std::initializer_list 형은 반환할 수 없다.



 그럼 먼저 auto의 타입 추론과 템플릿의 타입 추론이 얼마나 같은지(사실 약간 다르다) 간단한 설명을 보도록 하자. 먼저 템플릿 함수의 전형적인 예는 아래와 같다.

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

f(expr); // expr은 임의의 표현식

 일반적인 auto 선언문은 아래와 같은 형태이다. 

auto x = expr; // expr은 임의의 표현식

 컴파일러는 템플릿 함수 f()에 넘기는 인자 expr의 타입에 기초하여 T의 타입 추론을 수행한다. 이 때는 "템플릿 타입 추론" 방식으로 동작하게 되고 그 동작에 대한 설명은 여기를 참조하기 바란다. 자 그 다음 auto의 예를 보면 바로 느낌이 올 것이다. auto에서도 역시 expr의 타입에 기초하여 타입 추론을 수행하는데 그 규칙은 템플릿의 그것과 동일하다. 즉, auto가 템플릿의 T역할을 하는 것이다. 

 간단히 몇 가지 비교를 해보면 이해가 더 쉬울 것이다. 아래 예를 보자.

template<typename T>
void f1(T param);

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

template<typename T>
void f3(const T& param);

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

auto a1 = expr;        // f1(expr)과 같은 타입 추론 수행
auto& a2 = expr;       // f2(expr)과 같은 타입 추론 수행
const auto& a3 = expr; // f3(expr)과 같은 타입 추론 수행
auto&& a4 = expr;      // f4(expr)과 같은 타입 추론 수행

 위 예에서 auto와 T의 역할을 비교해보면 auto가 "템플릿 타입 추론" 방식으로 결정된다는 게 별로 이상한 일이 아님을 알 수 있을 것이다. auto의 타입 추론 방식은 심지어 배열과 함수가 지정됐을 때 조차도 템플릿과 같은 방식으로 타입을 결정한다. 아래 예를 보자.

const char name[] = "R. N. Briggs"; // name의 타입은 const char[13]
auto arr1 = name; // arr1의 타입은 const char*
auto& arr2 = name; // arr2의 타입은 const char(&)[13]

void someFunc(int, double); // someFunc는 function
                            // 타입은 void(int, double) 
auto func1 = someFunc; // func1의 타입은 void (*)(int, double)
auto& func2 = someFunc; // func2의 타입은 void (&)(int, double)

 템플릿 타입 추론에서 비-참조형과 참조형의 인자에 함수나 배열을 넘겼을 때의 동작은 auto 타입 추론에서도 그대로 적용된다. 즉 비-참조형 인자에 함수나 배열을 넘기면 포인터형으로 바뀌어 추론된다. 이 정도면 "auto 타입 추론"은 "템플릿 타입 추론"과 같다고 할 만 하다.

 하지만 auto는 템플릿 타입 추론과 한 가지 다른 점이 있는데 바로 braced initializer가 초기화값으로 주어졌을 때이다. 아래 예를 보면 auto의 타입이 예상과 다르게 결정되는 걸 볼 수 있다.

int x1 = 27;
int x2(27);
int x3 = { 27 }; // C++11, uniform initialization
int x4{ 27 }; // C++11, uniform initialization

auto a1 = 27; // 타입은 int, 값은 27
auto a2(27);  // 타입은 int, 값은 27
auto a3 = { 27 }; // 타입은 std::initializer_list<int>, 값은 {27}
auto a4{ 27 };    // 타입은 std::initializer_list<int>, 값은 {27}

 C++11에서 소개된 uniform initialization 덕분에 위의 x1, x2, x3, x4가 모두 가능한 선언이다. 하지만 똑같은 방식으로 auto 선언문으로 바꾸면 결과는 달라진다. 주석에서 볼 수 있듯이 a3, a4는 int형이 아니고 std::initializer_list<int>형으로 추론되기 때문이다. braced initializer를 이미 사용해 본 적이 있다면 컴파일러가 상황에 맞춰서 그 값들을 std::initializer_list형으로 생성한다는 걸 이미 알고 있을 것이다. auto 선언문에서도 컴파일러는 braced initializer를 std::initializer_list형으로 바꿔서 타입 추론을 수행한다. 컴파일러의 그런 동작을 이미 알고 있었다면 어느 정도 이해가 되는 상황일 것이다.

 위와 같이 auto는 braced initializer를 std::initializer_list형으로 인식하고 타입 추론을 수행하지만 템플릿에 braced initializer를 넘기면 타입 추론을 하지 못하고 컴파일에 실패한다. 이 점이 두 타입 추론의 차이점이다. 즉 아래 예에서 f()의 호출은 실패한다. 

// auto 타입 추론
auto x = { 11, 23, 9 }; // x의 타입은 std::initializer_list형으로 추론됨

// 템플릿 타입 추론
template
void f(T param);

f({ 11, 23, 9 }); // error! 타입 추론을 할 수 없음.


 C++14에서는 한 가지 더 짚고 가야할 것이 있다. C++14부터는 auto를 함수의 return 타입과 람다의 인자에서도 사용이 가능한데 이 때는 braced initializer를 std::initializer_list형으로 인식하지 않고 컴파일을 실패하게 된다. 즉 아래 두 가지 경우에 대해서는 (auto 타입 추론이 아닌) "템플릿 타입 추론"을 수행한다.

auto createInitList() {
    return { 1, 2, 3 }; // error: { 1, 2, 3 }을 타입 추론할 수 없음.
}
std::vector<int> v;
...

auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
...

resetV({ 1, 2, 3 }); // error! { 1, 2, 3 }을 타입 추론할 수 없음.

 위의 두 예에서 모두 auto는 에러가 난다. "템플릿 타입 추론"과 똑같이 braced initializer를 통해서 타입 추론을 하지 못하는 것을 볼 수 있다. 이렇게 보면 auto가 타입 추론되는 방식은 변수를 선언할 때만 braced initializer를 인식(std::initializer_list)한다는 점을 제외하면 템플릿 타입 추론과 완전히 동일하다.

 마지막으로 간단히 요약해 보자면...

  1. auto 타입 추론은 템플릿 타입 추론과 거의 같지만 braced initializer를 std::initializer_list형으로 인식해서 추론을 수행한다는 점이 다르다. (템플릿 타입 추론은 braced initializer를 통해서는 타입 추론을 실패함)
  2. auto가 함수의 return 타입, 람다의 인자에 사용되었을 때는 템플릿 타입 추론과 완전히 동일하게 동작한다.(즉 braced initializer를 통한 타입 추론은 실패한다)


( 퍼온글입니다.. 링크 : http://progtrend.blogspot.com/2017/02/auto-type-deduction-auto.html )


2. Universal reference

 type&& 이슈의 핵심은 때로는 rvalue reference를 의미하나, 때로는 rvalue 또는 lvalue reference로 해석된다는 것이다.

분명히 구문상 생긴 것은 rvalue reference인 것 같은데, 실제 의미는 lvalue reference일 수 있다는 것이다.
이렇게 특이한 유연성을 가지는 reference를 가리켜, universal reference라고 한다.

type&&이 universal reference를 가리킬 때의 세부 사항들은 상당히 tricky 하기에, 전반적인 세부 설명은 일단 뒤로 미루겠다.
(3. Reference collapsing 챕터 참고)
지금은 당장 알아야 되는 규칙들부터 살펴보기로 하자.

변수나 인자가 type deduction(추론)이 필요하며, 정확히 "T&&"로 선언되면, 해당 변수나 인자는 universal reference이다.

변수가 타입 추론이 필요한 경우는 auto로 선언된 변수이고, 인자가 타입 추론이 필요한 경우는 템플릿 함수의 인자인 경우다.
참고로, 위 둘의 타입 추론 규칙은 완벽하게 동일하며, 템플릿 함수의 인자로 훨씬 많이 사용된다.

후반부의 3. Reference collapsing 챕터에서는 universal reference를 typedef와 decltype과 함께 사용하는 것에 대해 설명할 것이다. 하지만, 그 이전에는 universal reference가 템플릿 함수 인자나 auto 변수로 사용되는 것에 한정해서 설명을 진행해 나가겠다.


2-1. auto&&

모든 레퍼런스와 마찬가지로 universal reference 역시 반드시 초기화가 되어야 하며,
universal reference는 그것이 rvalue reference인지 lvalue reference인지 결정하는 initializer가 존재한다.

Universal reference는 추론될 타입이 rvalue reference인가? lvalue reference인가?에 따라 &&를 해석한다.

다음의 예제를 살펴보자.

  1. Widget&& var1 = someWidget;
  2. auto&& var2 = var1;

위 예제에서 var1의 주소를 알 수 있으므로, var1 자체(변수 그 자체)는 lvalue이다.
var2의 타입은 auto&&로 선언되었기에 universal reference이고, var1이 lvalue이므로 var2는 lvalue reference로 해석된다.
아무 생각없이 코드를 읽다보면, var2가 rvalue reference라고 읽을 수 있는 함정이 숨어 있는 것이다.

즉, 위 예제의 2라인은 다음과 동일하다 할 수 있다.

  1. Widget& var2 = var1;

또다른 예제를 하나 살펴보도록 하자.

  1. std::vector<int> v;
  2.  
  3. auto&& val = v[0];

val은 auto&&로 선언되어 universal reference이고, std::vector<int>::operator[]의 결과인 v[0]으로 초기화 되었다.
std::vector<T>::operator [] 는 lvalue를 반환하므로, val 역시 lvalue reference이다.


2-2. T&& as template function argument

앞서 universal reference는 템플릿 함수 인자로써 훨씬 더 많이 사용된다고 하였다.
위의 템플릿 함수 예제를 한번 더 살펴보자.

  1. template <typename T>
  2. void f(T&& param);
  3.  
  4. // 주소를 취할 수 없는 literal 10은 rvalue
  5. // 따라서, param은 rvalue reference (int&&)
  6. f(10);
  7.  
  8. // 주소를 취할 수 있는 x는 lvalue
  9. // 따라서, param은 lvalue reference (int&)
  10. int x = 10;
  11. f(x);

위 예제의 f 함수는 무엇이 인자로 넘어오는지에 따라, param은 rvalue reference가 될 수도, lvalue reference가 될 수도 있다.
이럴 경우 param이 universal reference인 것이다.

"&&"이 universal reference를 의미하는 경우는 타입 추론이 필요한 경우만이라는 것을 잊지 말아야 한다.
이 말을 뒤집으면, 타입 추론이 아닌 명시가 되어 있는 경우에는 "&&"는 늘 rvalue reference를 의미한다는 소리다.

  1. template <typename T>
  2. void f(T&& param);           // T에 대한 타입 추론이 필요. && ≡ universal reference
  3.  
  4. template <typename T>
  5. class Widget
  6. {
  7.     ...
  8.     Widget(Widget&& rhs);    // 명시적인 Widget 타입. && ≡ rvalue reference
  9. };
  10.  
  11. template <typename T1>
  12. class Gadget
  13. {
  14.     ...
  15.     template <typename T2>
  16.     Gadget(T2&& rhs);        // T2에 대한 타입 추론이 필요. && ≡ universal reference
  17. };
  18.  
  19. void f(Widget&& param);      // 명시적인 Widget 타입. && ≡ rvalue reference

지금까지의 내용을 제대로 이해했다면, 위 예제는 전혀 의문을 품을 것이 없다.
타입을 추론해야 하는 경우 &&는 universal reference이고, 타입이 명시적인 경우엔 &&는 rvalue reference인 것이다.


2-3. 혼동하기 쉬운 경우들

그러면, 앞서 나왔던 다음의 예제는 어떻게 될까?

  1. template <typename T>
  2. void f(std::vector<T>&& param); // rvalue ref

위 f 함수의 인자는 추론해야 할 타입 T도 있고, "&&"도 붙어있다.
하지만, T&&가 아닌 std::vector<T>&&이므로 이 녀석의 타입은 명시적으로 std::vector<T>에 대한 &&이다.
따라서, universal reference가 아닌 일반적인 rvalue reference인 것이다.

Universal reference가 템플릿 함수 인자로 사용될 때엔 반드시 "T&&"의 형태여야 한다.
여기에 const 한정자만 가져다 붙여도 "&&"는 universal reference로 해석되지 않는다.

  1. template <typename T>
  2. void f(const T&& param);    // rvalue ref


또한, T가 템플릿 인자이고, 템플릿 함수 인자가 T&&여도 T에 대한 타입 추론이 발생하지 않는 경우도 있다.

  1. template <typename T, type Allocator = allocator<T> >
  2. class vector
  3. {
  4. public:
  5.     ...
  6.     void push_back(T&& x);    // 이미 T의 타입을 알기에, 타입 추론이 발생하지 않는다. && ≡ rvalue reference
  7. };

위 예제에서 T는 템플릿 인자이고, push_back 함수는 T&&를 함수 인자로 받는다.
하지만, push_back 함수는 std::vector의 객체가 존재하지 않으면 호출될 수 없다.

즉, std::vector<T>의 객체가 생성되기 위해, instantiate 되는 과정에서 T는 이미 타입 추론이 완료되었기에, 
push_back이 호출되는 시점에서 다시 T에 대한 타입 추론을 할 필요가 없는 것이다.
따라서, push_back의 "T&&"는 universal reference가 아닌, rvalue reference 이다.

보충 설명이 될 수 있는 예제를 하나 더 살펴보자.

  1. // Widget 객체를 생성하는 팩토리 함수
  2. Widget makeWidget();
  3.  
  4. // Widget 벡터 생성
  5. std::vector<Widget> vw;
  6.  
  7. Widget w;
  8.  
  9. // 이 함수를 풀어쓰면 다음과 같다.
  10. // std::vector<Widget>(Widget&& w);
  11. // 이미 이 시점에서 Widget 타입이라는 것이 명시되어 있으므로, 타입 추론할 필요가 없다.
  12. vw.push_back(makeWidget());

push_back과 대조적으로 emplace_back의 경우는 다음과 같이 정의되어 있다.

  1. template <typename T, typename Allocator = allocator<T> >
  2. class vector
  3. {
  4. public:
  5.     ...
  6.     template <class... Args>
  7.     void emplace_back(Args&&... args); // 각 Args들의 타입 추론이 필요. && ≡ universal references
  8. };

emplace_back 함수는 variadic template 함수로 구현되어 있어, 가변수의 인자를 받는다.
따라서, 가변수의 개별 인자 타입들은 모두 타입 추론이 필요하다.
다음처럼 외부 함수 선언을 해보면, 이해가 더 쉽게 될 것이다.

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

결과적으로 emplace_back의 모든 인자들은 모두 universal reference인 것이다.
push_back의 인자가 명백히 rvalue reference였던 것과는 대조적이다.


2-4. lvalueness or rvalueness

마지막으로 기억해야 할 중요 포인트는 현식이 lvalueness 또는 rvalueness를 가지느냐는 그것의 타입에 의존적이라는 것이다.

int 타입이 변수로 선언되면 lvalue이지만, literal 값이면 rvalue이다.
Widget 타입이 변수로 선언되면 lvalue이지만, 팩토리 함수등이 반환한 임시값이면 rvalue이다.

표현식이 lvalue냐 rvalue냐는 그것의 타입에 의존적이기 때문에, 
타입은 rvalue reference이지만, 표현식 자체(변수일 경우 해당 변수값)는 lvalue 또는 rvalue가 될 수 있다.

  1. // Widget을 생성하는 팩토리 함수
  2. Widget makeWidget();
  3.  
  4. // var1의 타입은 rvalue reference이지만, var1 자체는 lvalue
  5. Widget&& var1 = makeWidget();
  6.  
  7. // 여기에서의 var1은 lvalue
  8. // var2의 타입은 rvalue reference이지만, var2 자체는 lvalue
  9. Widget&& var2 = std::move(var1);

rvalue reference 타입이지만, 이름이 붙어 있는 변수나 인자들은 주소를 획득할 수 있기에 lvalue 들이다.
다음의 예제를 다시 한번 살펴보도록 하자.

  1. template <typename T>
  2. class Widget
  3. {
  4.     ...
  5.     Widget(Widget&& rhs);    // rhs의 타입은 rvalue reference,
  6.     ...                      // 하지만, rhs 자체는 lvalue
  7. };
  8.  
  9. template<typename T1>
  10. class Gadget
  11. {
  12.     ...
  13.     template <typename T2>
  14.     Gadget(T2&& rhs);        // rhs의 타입은 universal reference
  15.     ...                      // 하지만, rhs 자체는 lvalue
  16. };

Widget의 생성자 함수에서 rhs는 rvalue reference이기에, rhs가 rvalue로 bound 될 것이라는 걸 알지만 rhs 자체는 lvalue이다.
따라서, 무엇이 rhs로 넘겨지든 rvalueness의 이점을 유지하기 위해 rhs를 rvalue로 변환할 필요가 있다.
이것이 바로 std::move를 사용해야 하는 일반적인 동기(motivation)가 된다.

Widget의 경우와 유사하게 Gadget의 생성자 함수에서 rhs는 universal reference이다.
rhs는 rvalue 또는 lvalue로 bound 될 수 있으나, rhs 자체는 lvalue이다.
rhs가 rvalue로 bound될 경우엔 rvalueness의 이점을 유지해야 하고, lvalue로 bound될 경우엔 lvalueness를 유지시켜 줘야 한다.
이것이 바로 std::forward를 사용해야 하는 일반적인 동기(motivation)가 된다.
즉, rvalue는 rvalue로, lvalue는 lvalue로 bound 시켜야 할 때 std::forward를 사용하는 것이다.


3. Reference Collapsing

C++11의 특정한 생성 과정에서 레퍼런스의 레퍼런스가 발생하지만, C++은 레퍼런스의 레퍼런스를 허용하지 않는다.
만약, 소스코드 자체에 명시적으로 레퍼런스의 레퍼런스 형식을 포함한다면 그 코드는 유효한 코드가 아니다.

  1. Widget w1;
  2. // 에러! 레퍼런스의 레퍼런스 따윈 없는거임!
  3. Widget& & w2 = w1;

하지만, 컴파일러가 타입을 추론하는 과정에서의 결과로써, 레퍼런스의 레퍼런스가 발생하는 경우들이 있다.
이렇게 발생한 레퍼런스의 레퍼런스에 대해 reference collapsing을 수행하게 되며,
이것이 바로 universal reference가 lvalue reference 또는 rvalue reference로 해석될 수 있는 기본 메커니즘이다.


3-1. Template function arguments

Universal reference 타입을 템플릿 인자로 받아 타입을 추론하는 과정에서, lvalue와 rvalue는 조금 다르게 타입이 추론된다.
T의 lvalue 타입은 T&로써 추론되나, T의 rvalue 타입은 그냥 T로 추론된다.

템플릿 함수 인자의 타입이 universal reference이고, 이 인자로 lvalue와 rvalue를 받을 때 각각 어떤 일이 발생하는지 조금 더 살펴보기로 하자.

  1. template<typename T>
  2. void f(T&& param);
  3.  
  4. ...
  5.  
  6. int x;
  7.  
  8. ...
  9.  
  10. // rvalue로 f 함수 호출
  11. f(10);
  12. // lvalue로 f 함수 호출
  13. f(x);

숫자 리터럴 10으로 f 호출시, T는 int로 추론되며 f 함수는 다음과 같이 instantiate된다.

  1. // rvalue로부터 f 함수 instantiated
  2. void f(int&& param);

하지만, 변수 x, lvalue로 f 호출시 T는 int&로 추론되며, f 함수는 레퍼런스의 레퍼런스를 포함한 채 instantiate된다.

  1. // lvalue로부터 f 함수 instantiated
  2. void f(int& && param);

레퍼런스의 레퍼런스로 인해 얼핏 보기엔 유효하지 않은 코드같지만, 
이는 명시적으로 유저가 입력한 것이 아닌, 컴파일러가 타입을 추론하는 과정에서 생긴 것이며,
이 과정에서의 문제를 회피하기 위해 앞서 소개한 "reference collapsing"을 수행한다.

lvalue reference와 rvalue reference, 두 종류의 레퍼런스 종류로 인해 4가지의 레퍼런스의 레퍼런스 조합이 존재할 수 있다.
  • &    &    (L + L)
  • &    &&  (L + R)
  • &&  &    (R + L)
  • &&  &&  (R + R)

그리고, 두 가지의 reference collapsing 규칙이 다음과 같이 존재한다.
  • &&  &&  (R + R) 은 &&로 collapse 된다.
  • 그 외 나머지 조합(L 이 하나라도 포함된)은 &로 collapse 된다.

즉, 위 collapse 규칙을 적용하면 다음과 같이 된다.
  • &    &    (L + L) -> &
  • &    &&  (L + R) -> &
  • &&  &    (R + L) -> &
  • &&  &&  (R + R) -> &&

위 규칙을 lvalue로 f 함수가 instantiate되는 상황에 적용하면, 컴파일러는 최종적으로 아래처럼 해석하여 적법한 코드가 된다.

  1. // reference collapsing 이후 lvalue로부터 f 함수 instantiated
  2. void f(int& param);


지금까지의 내용보다 훨씬 더 미묘한 경우가 있는데, 바로 변수의 타입이 레퍼런스인 경우이다.
이 경우 타입의 레퍼런스 부분이 무시(reference-stripping)된 채 처리된다. 다음의 예제를 살펴보자.

  1. template<typename T>
  2. void f(T&& param);
  3.  
  4.  
  5. int x;
  6.  
  7.  
  8. // r1의 타입은 int&&
  9. int&& r1 = 10;
  10.  
  11. // r2의 타입은 int&
  12. int& r2 = x;

r1과 r2 모두 템플릿 함수 f를 호출하는 과정에서 먼저 int로 간주된다.
위 경우처럼 레퍼런스 부분을 무시(reference-stripping)하고 타입을 처리할 때는 universal reference의 타입을 lvalue는 T&로, rvalue는 T로 추론하는 규칙으로부터 독립적이다. (즉, 그 규칙에 영향을 받지 않는다)

따라서, 위 예제는 다음과 같이 컴파일러는 해석한다.

  1. f(r1);
  2.  
  3. f(r2);

최종적으로 템플릿 함수 f에서 r1과 r2는 param으로 넘어갈 때 모두 int&로 추론된다.
맨 처음 r1과 r2의 reference-stripping으로 인해 int로 간주되었고, 그 결과 r1과 r2는 lvalue 이기 때문에, 템플릿 함수 f의 최종 인자로 넘어갈 땐 universal reference의 타입 추론 규칙에 의해 int&로 간주되는 것이다.


3-2. auto variable

위에서 언급했듯이 reference collapsing이 발생하는 경우는 대부분 템플릿 함수가 instantiate될 때 발생한다.
또 하나의 경우가 있는데, auto 변수가 선언되는 경우이다.

auto 변수의 universal reference를 타입 추론할 때 lvalue는 T&로, rvalue는 T로 추론된다.
(템플릿 함수의 인자에 대해 universal reference를 타입 추론하는 것과 동일하다)
앞서 나왔던 예제를 다시 한번 살펴보자.

  1. // var1은 Widget&& 타입
  2. Widget&& var1 = someWidget;

  3. auto&& var2 = var1;

var1의 타입은 Widget&&이지만, var2의 초기화 과정에서 타입이 추론될 때 var1의 referenceness는 무시된다.
템플릿 인자 처리에서처럼 변수 타입이 reference인 경우 reference-stripping을 수행, var1은 Widget 타입으로 간주되는 것이다.

var2의 universal reference를 초기화하는데 var1이 lvalue로 간주되면서, var2의 universal reference는 Widget&로 추론된다.
따라서, 최종적으로 컴파일러는 다음과 같이 reference collapsing을 수행한다.

  1. // reference-stripping으로 인해 먼저 var1은 대입되기 전 Widget 타입
  2. // Widget var1은 lvalue이므로, lvalue reference로 타입 추론
  3. Widget& && var2 = var1;
  4.  
  5. // 최종적으로 다음과 같이 reference collapsing이 발생
  6. Widget& var2 = var1;


3-3. typedef

세번째 reference collapsing이 발생하는 경우는 typedef를 사용하는 경우이다.

  1. template <typename T>
  2. class Widget
  3. {
  4.     typedef T& LValueRefType;
  5.     ...
  6. };
  7.  
  8. Widget<int&> w

객체 w는 다음과 같이 instantiate 된다.

  1. // instantiate 된 직후의 typedef
  2. typedef int& & LValueRefType;
  3.  
  4. // reference-collapsing이 발생한 이후
  5. typedef int& LValueRefType;

이 상태에서 다음과 같이 Widget을 사용하게 되면..

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

이는 다음과 같이 해석되고,

  1. void f(int& && param);

최종적으로 다음과 같이 reference collapsing이 발생한다.

  1. void f(int& param);


3-4. decltype


마지막으로 살펴볼 reference collapsing 사례는 decltype의 사용이다.

템플릿 함수 인자 추론이나 auto 변수 추론의 경우처럼, decltype(표현식) 역시 표현식에 대한 T 또는 T&로의 타입 추론을 수행하며, 이후 C++11의 reference collapsing 규칙을 적용한다.

아~ 애석하게도, decltype에 적용된 타입 추론 규칙은 템플릿이나 auto의 타입 추론 규칙과 동일하지 않다.
세부 내용은 너무 불가사의하기에(arcane) 여기에서 모든 내용을 다루진 않겠다.
(5. 추가 링크의 decltype 관련 링크 확인)

하지만, 주목할만한 차이점 하나는 non-reference 타입의 이름 있는 변수에 대해, 템플릿과 auto가 T&로 추론하는 것과 달리 decltype은 T로 추론한다.

또다른 중요 차이점 하나는 decltype의 타입 추론은 표현식에만 의존한다.
표현식이 초기화되는(initializing) 과정에서의 타입은 무시된다.

  1. Widget w1, w2;
  2.  
  3. // v1은 lvalue로부터 초기화되는 auto-based universal reference
  4. // 따라서, v1의 타입은 w1을 참조하는 lvalue reference
  5. // Widget& v1 = w1;과 동일하다
  6. auto&& v1 = w1;

  7.  
  8. // v2는 decltype-based universal reference
  9. // w2가 어떤 타입인지 관계없이, decltype(표현식)의 표현식에만 의존해 타입이 결정된다
  10. // decltype(w1)은 Widget이므로(Widget&이 아님), v2의 타입은 rvalue reference
  11. // 즉, Widget&& v2 = w2;와 동일하다.
  12.  
  13. // 하지만, w2는 lvalue
  14. // rvalue reference를 lvalue로 초기화할 수 없으므로 아래 코드는 컴파일 에러 발생
  15. // decltype(w1)&& v2 = std::move(w2);로 해야 적법한 코드가 된다
  16. decltype(w1)&& v2 = w2;

9~12라인의 주석을 곱씹어보면, decltype-based universal reference를 이해하기 쉬울 것이다.


4. 정리

타입 선언시 "&&"는 rvalue reference 또는 universal reference를 둘 중 하나로 해석될 수 있다.
그리고, universal reference는 lvalue reference 또는 rvalue reference 둘 중 하나로 해석될 수 있다.

Reference collapsing은 universal reference가 lvalue reference 또는 rvalue reference로 해석될 수 있는 메커니즘을 제공한다. 이는 컴파일 과정에서 레퍼런스의 레퍼런스가 발생하는 특수한 상황에서 수행된다.
  • 템플릿 함수 인자 추론
  • auto 변수 추론
  • 레퍼런스 typedef
  • decltype 표현식 추론


(이것도 퍼온글... 링크 : http://egloos.zum.com/sweeper/v/3149089)




퍼온것들 꼭 마스터 해야징 야호