Post List

2015년 2월 4일 수요일

item 03: decltype에 대해 알아보자.

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점









Effective Modern C++ - Scott Meyers
Item 3: Understand decltype.

item 3. decltype에 대해 알아보자.

* Intro

decltype은 이상한 녀석이다.
이름(name)이나 표현식(expression)으로부터 타입(type)을 얻을 수 있다.
대부분의 경우 예측한 그대로 정확하게 type을 알려준다. (이게 잴 중요하다. 고로 이번 item은 공부 할 필요가 없다.)
하지만, 가끔씩 뒷통수를 칠만큼 이상한 결과를 주기도 한다. (젠장)

decltype의 간단한 예제는 다음과 같다.
(autotemplate 의 타입 추론 동작과 비교를 잘 해보길 바란다.)

const int i = 0;              // decltype(i) : const int

bool f(const Widget& w);      // decltype(w) : const Widget&
                              // decltype(f) : bool(const Widget&)

if (f(w)) ...                 // decltype(f(w)) : bool

struct Point { int x, y; };   // decltype(Point::x) : int
                              // decltype(Point::y) : int
Widget w;                     // decltypw(w) : Widget

template<typename T>   // std::vector 간단한 구현
class vector {
public:
...
T& operator[](std::size_t index);
...
};

vector<int> v;                // decltype(v) : vector<int>
if (v[0] == 0) ...            // decltype(v[0]) : int&

다들 이해가 쉽게 될 것이다.

* Trailing return type syntax - C++ 11

C++11 에서는  decltype을 template function에서 parameter 타입에 따라 return을 추론해야 할 경우에 많이 사용된다.
예를 들어 [ ] 를 이용하여 container의 요소하나를 반환하는 경우를 생각해보자.
다른 이유 (예를 들어서 보안상 문제)에 의해서 해당 기능을 함수로 wrapping 하는 경우 함수의 return 타입은 [ ] 연산 결과와 반드시 일치해야 한다.
[ ] 연산의 return 타입은 타입 T 에 대한 참조인 T&이다. 왜냐면 참조형이어야 해당 값을 수정할수 있으니깐. (당연한 이야기이다.)
(대표적인 예로는 std::deque<T>가 있다. std::vector<bool> 의 경우는 bool& 이 아닌 다른 객체가 return 되는데, item 6 에서 다루겠다.)
잠깐 삼천포로 빠졌는데, 어쨌거나 중요한건 [ ]의 return 타입은 T& 여야 한다는 것이다.

template <typename C// Container
          typename I>  // Index
auto authAndAccess(C& c, I i)
-> decltype(c[i]) // trailing return type (using parameter)
{
    if (IsAuthenticatedUser())
       return c[i];
}

C++11 에서는 함수의 return에 사용된 auto는 아무런 추론도 하지 못한다.
대신 trailing return type 기법을 적용하여 -> 뒤에 리턴 타입을 지정한다.'
이것의 장점은 함수의 파라메터를 이용하여 리턴 타입을 만들 수 있다는 것이다.
리턴 타입이 파라메터 앞에 있으면 파라메터의 변수가 선언되기 전이라서 컴파일러가 인식을 하지 못한다.

C++11에서 리턴 타입의 추론은 단일 구문(single-statement), 즉 return이 하나인 Lambda 만을 허용한다.

[=]() -> int { return 27; };

C++14에서는 다중 구문을 사용한 Lambda 및 모든 함수에서의 타입 추론이 가능해 졌으며, trailing return type의 생략이 가능하다.

[=](bool b) { if (b == true) return func1();
              else return           func2(); };

* Return type deduction - C++ 14

template <typename C// Container
          typename I>   // Index
auto authAndAccess(C& c, I i)
{
    if (authenticateUser())
       return c[i];   // return type deduced from c[i];
}

C++14에서 다중 구문 추론이 가능하다고 해서 문제가 다 해결되는건 아니다.
1. item 2 에서 auto 를 이용하여 리턴 타입을 추론할 경우 template 타입 추론을 이용한다고 배웠다.
2. 하지만 item 1 에서 template 타입 추론을 할 때 reference (&) 부분은 무시한다고 배웠다.
그럼 위 함수는 어떻게 될까 ? 우리가 원하는대로 특정 요소를 가져와서 변경이 가능할까?

std::deque<int> d;
...
authAndAccess(d, 5) = 10; // return d[5] (not int&)
                          // error C2016 : '=' left operand must be l-value

컴파일 오류가 발생한다. 위 1, 2 과정을 거치면서 int&가 아닌 d[5] 값 자체인 r-value를 가져오기 때문이다.

그럼 제대로 돌아가게 할수 있는 방법은 없는걸까 ?
가만 ! 분명 이번 item은 decltype에 관한 내용일껀데 계속 auto 얘기만 하고 있었다.
이제 decltype 을 등장시켜서 계속 이야기를 해 보겠다.

* decltype(auto)

template <typename C// Container
          typename I// Index
decltype(auto) authAndAccess(C& c, I i)
{
    if (authenticateUser())
       return c[i];
}

decltype와 auto가 콤보로 사용되었다.
auto 는 타입 추론을 자동으로 해주겠다는 뜻이고,
decltype 는 추론을 decltype 방식대로 할 것이다라는 뜻이다.

이렇게 해주면 c[i]가 T& 타입을 리턴하므로 우리가 원하는대로 동작하게 된다.

decltype(auto)는 함수 리턴 타입만을 위한 구문은 아니다.
변수선언을 할때도 decltype 추론 규칙을 사용하고 싶을때는 얼마든지 사용이 가능하다.

Widget w;
const Widget& cw = w;

auto           myWidget1 = cw; // auto     type deduction : Widget
decltype(auto) myWidget2 = cw; // decltype type deduction : const Widget&

다시 원래 문제로 돌아와서...
아직 authAndAccess 함수는 완벽하지 못하다.
2가지를 좀 더 고려해야하는데...

template <typename C,  // Container
          typename I>  // Index
decltype(auto) authAndAccess(CcI i);

위 경우 C에는 l-value 참조자를 대입해야만 한다. (그것도 const가 없는 녀석으로만)
왜냐면 함수의 리턴 타입으로 수정가능한 컨테이너 내부 원소를 넘기기 때문이다.
즉, r-value 를 C 로 넘길수 없다는 말이다.
물론 r-value인 컨터에너를 함수로 전달하는 것은 드문 일이긴 하지만, 가끔 임시 컨테이너에 데이터를 복사하는 경우도 발생할 수 있다.
(솔직히 r-value 컨터에너를 사용하는건 위험하다. r-value 특성상 구문이 끝나면 객체가 소멸되는데, 이런 객체의 내부 값을 참조하다간 dangling 객체가 생겨버린다.)

r-value 컨테이너를 사용하는 예제를 한번 살펴보자.

template <typename T>
std::deque<T> makeDeque()         // factory function
{ return std::deque<T>(20, 0); };

auto s = authAndAccess(makeDeque<std::string>(), 5);

위와 같이 r-value도 지원을 하게 할려면 r-value, l-value용 함수를 2개를 오버로딩을 통해 만들 수도 있다.
하지만 2개를 유지보수 해야한다는 불편함이 발생한다.

Universal reference를 사용하면 l-value, r-value를 다 받을 수 있다. (item 24)

template <typename C,  // Container
          typename I>  // Index
decltype(auto) authAndAccess(C&& cI i);

그럼 이제 해결 끝 ?
아니다.
함수 내부에서 C가 l-value 인지 r-value인지 고려하지 않고 동작하게 된다.
우리가 모르는 컨테이너의 알려지지 않은 타입에 대해서 사용을 한다면, 그것도 복사작업을...
아무런 성능 상의 문제를 일으키지 않으리라고 보장 할 수 있을까 ?
더군다나 object slicing (item 41) 과 같은 문제라도 발생한다면 ???

그래도 일단 STL의 컨테이너를 사용하는 경우는 복사 성능에 큰 문제가 없다.
(std::vector, std::string, std::deque 등...)
하지만 실제 template로 이를 구현할 때에는 universal reference에 std::forward를 적용시켜주면 해결 된다.

std::forward의 역할은 l-value는 l-value로 전달하고, r-value는 r-value로 전달해준다.

template <typename C// Container
          typename I// Index
decltype(auto) authAndAccess(C&& c, I i)
{
    if (authenticateUser())
       return std::forward<C>(c)[i];
}

이제 진짜 최종 완성된 버전이다.
하지만, C++14용 버전이다.
C++11에서는 아래와 같이 사용하면 된다.

template <typename C// Container
          typename I>  // Index
auto authAndAccess(C&& c, I i)
-> decltype(std::forward_as_tuple<C>(c)[i])
{
    if (authenticateUser())
       return std::forward<C>(c)[i];
}

* decltype(expr)

전문 라이브러리 개발자가 아니라면 이러한 특수한 경우까지는 고려하지 않고 decltype를 사용해도 괜찮다.

특정 'name'에 decltype를 적용하면 해당 'name'의 타입을 반환한다.
'name' 자체는 l-value 이므로 'name'을 이용한 표현식(expression)도 l-value이다.
하지만 'name'의 표현식에 decltype를 적용하면 조금 다르게 동작한다.

decltype(auto) f1()
{
    int x = 0;
    return x;      // decltype(x) : int
}

decltype(auto) f2()
{
    int x = 0;
    return (x); // decltype((x)) is int&
}

auto a1 = f1(); // int
auto a2 = f2(); // int

decltype(auto) a3 = f1(); // int
decltype(auto) a4 = f2(); // int&

가장 간단한 표현식으로 단순히 괄호 ( ) 를 씌운 것을 생각할 수 있다.
표현식을 적용한 'name' 의 decltype 타입 추론 결과는 참조형 (&) 이라는 것을 알 수 있다.
f2 함수의 결과를 받는 쪽에서도 변수 선언에 decltype을 적용해야지만 참조형으로 선언이 된다.
사실 그닥 신경쓰지 않아도 되는 내용이긴 하다.
위 예제에서 보는것과 같이 대부분의 경우 일반형에 참조형을 넣어도 오류가 발생하지 않는다.

일단 l-value에 대해서는 알아봤는데, 그럼 r-value는 어떻게 처리가 될까 ?
아래 그림을 보자.

(expr == NAME*)  ? AS DECLARED :
(expr == lvalue) ? T&// l-value
(expr == xvalue) ? T&& : // r-value -> xvalue       
                   T     // r-vlaue -> prvalue

* NAME : plain, unparenthesised variable, function parameter, calss member access
* xvalue
 - 함수의 리턴 타입이 r-value인 function call
   e.g. std::move(x)
 - r-value reference의 static cast
   e.g. static_cast<A&&>(a)
 - xvalue의 member access
   e.g. static_cast<A&&>(a).m_x
* prvalue : xvalue가 아닌 나머지 r-value

물론 굳이 위에 것을 다 알아야만 하는건 아니다.



Things to Remember

decltype 는 대부분의 경우 변수나 표현식의 타입을 그대로 알려준다.
* T 타입에 대한 'name'이 아닌 l-value 표현식에 대하여 decltype은 항상 T&를 제공한다.
* C++14에는  decltype(auto)를 지원해주는데, auto와 비슷하지만 별도의 decltype 타입추론 규칙을 사용한다.