-----------------
title : 씹어먹는 C++ - <16 - 3. 타입을 알려주는 키워드 decltype 와 친구 std::declval>
cat_title : 16 - 3. 타입을 알려주는 키워드 decltype 와 친구 std::declval
path : /C++
publish_date : 2019-09-15
next_page : 295
chapter : decltype 와 std::decval
-----------------

이번 강좌에서는

* `decltype` 키워드 설명
* C++ 의 값 카테고리 - \serif{lvalue, prvalue, xvalue}
* `std::declval` 함수 설명

에 대해서 다룹니다.

![](/img/ChewingCpplogo.png)

안녕하세요 여러분! 지난번 강좌에서 다룬 `constexpr` 는 잘 써먹고 계신가요? 그 동안 강좌에서 C++ 에서 사용되는 대부분의 키워드를 다루웠던 것 같은데 아직 하나 빠먹은 것이 있습니다. 바로 타입 관련 연산을 사용할 때 요긴하게 쓰이는 `decltype` 키워드 입니다.

### decltype

`decltype` 키워드는 C++ 11 에 추가된 **키워드**로, `decltype` 라는 이름의 함수 처럼 사용됩니다.

```cpp
decltype(/* 타입을 알고자 하는 식*/)
```

이 때, `decltype` 는 함수와는 달리, 타입을 알고자 하는 식의 타입으로 치환되게 됩니다. 예를 들어서

```cpp-formatted
#include <iostream>

struct A {
  double d;
};

int main() {
  int a = 3;
  decltype(a) b = 2;  // int

  int& r_a = a;
  decltype(r_a) r_b = b;  // int&

  int&& x = 3;
  decltype(x) y = 2;  // int&&

  A* aa;
  decltype(aa->d) dd = 0.1;  // double
}
```

위 코드의 경우 `decltype` 이 각각 `int, int&, int&&` 로 치환되서 컴파일 되게 됩니다. 위와 같이 `decltype` 에 전달된 식이 **괄호로 둘러쌓이지 않은 식별자 표현식(id-expression)** 이라면 해당 식의 타입을 얻을 수 있습니다.

참고로 식별자 표현식이란 변수의 이름, 함수의 이름, `enum` 이름, 클래스 멤버 변수(`a.b` 나 `a->b` 같은 꼴) 등을 의미합니다. 엄밀한 정의는 [여기](http://eel.is/c++draft/expr.prim.id#2.2) 에서 볼 수 있는데, **쉽게 생각하면 어떠한 연산을 하지 않고 단순히 객체 하나만을 가리키는 식**이라고 보시면 됩니다.

그렇다면 만약에 `decltype` 에 식별자 표현식이 아닌 식을 전달하면 어떨까요? 그렇다면 해당 식의 값의 종류(value category)에 따라 달라집니다.

* 만일 식의 값 종류가 \serif{xvalue} 라면 `decltype` 는 `T&&` 가 됩니다.
* 만일 식의 값 종류가 \serif{lvalue} 라면 `decltype` 는 `T&` 가 됩니다.
* 만일 식의 값 종류가 \serif{prvalue} 라면 `decltype` 는 `T` 가 됩니다.

잠깐! 지금 위 4 문장에서 너무나 많은 개념들이 나타났습니다. 값의 종류? \serif{xvalue}? \serif{lvalue}? \serif{prvalue}? 얘네들의 정체가 무엇인지 바로 알아보도록 하겠습니다.

### Value Category

사람의 경우 이름과 나이라는 정보가 항상 따라다니듯이, 모든 C++ 식(expression)에는 두 가지 정보가 항상 따라다닙니다. 바로 식의 **타입** 과 **값 카테고리(value category)** 입니다.

타입은 여태까지 공부하신 분들이라면 잘 알겠을 그 타입이 맞습니다. 하지만 값 카테고리가 뭔지는 약간 생소할 수 도 있는데, 이는 이전에 이야기 하였던 좌측값/우측값을 일컫는 것입니다. 하지만 C++ 에서는 사실 총 5 가지의 값 카테고리가 존재합니다.

C++ 에서 어떠한 식의 값 카테고리를 따질 때 크게 두 가지 질문을 던질 수 있습니다.

* **정체를 알 수 있는가?** 정체를 알 수 있다는 말은 *해당 식이 어떤 다른 식과 같은 것인지 아닌지를 구분할 수 있다*는 말입니다. 일반적인 변수라면 주소값을 취해서 구분할 수 있겠고, 함수의 경우라면 그냥 이름만 확인해보면 될 것입니다.
* **이동 시킬 수 있는가?** 해당 식을 다른 곳으로 안전하게 이동할 수 있는지의 여부를 묻습니다. 즉 해당 식을 받는 이동 생성자, 이동 대입 연산자 등을 사용할 수 있어야만 합니다.

이를 바탕으로 값 카테고리를 구분해보자면 아래 표와 같습니다.

||이동 시킬 수 있다|이동 시킬 수 없다|
|--|------------|--------------|
|**정체를 알 수 있다**|\serif{xvalue}|\serif{lvalue}|
|**정체를 알 수 없다**|\serif{prvalue}|쓸모 없음!|

덧붙여서 **정체를 알 수 있는** 모든 식들을 \serif{glvalue} 라고 하며, **이동 시킬 수 있는** 모든 식들을 \serif{rvalue} 라고 합니다. 그리고 C++ 에서 실체도 없으면서 이동도 시킬 수 없는 애들은 어차피 언어 상 아무런 의미를 갖지 않기 때문에 따로 부르는 명칭은 없습니다.\sidenote{각 값 카테고리를 단순하게 번역해보자면 lvalue 는 좌측값, prvalue (pure rvalue) 는 순수 우측값, xvalue (eXpiring value) 는 소멸하는 값, glvalue (generalized lvalue) 는 일반화된 좌측값, rvalue 는 우측값 이라 부를 수 있습니다. 근데 이렇게 한글 이름으로 부르는 것이 좀 더 헷갈리는 것 같아서 그냥 영문 이름으로 부르겠습니다.}

![caption=Value Category](/img/cpp/16.3.1.png)

위 그림을 보면 어떤 식으로 구분될 수 있는지 이해가 더 잘 될 것입니다.

#### lvalue

예를 들어서 평범한 `int` 타입 변수 `i` 를 생각해봅시다.

```cpp-formatted
int i;
i;
```

그리고 우리가 `i` 라는 식을 썼을 때, 이 식의 정체를 알 수 있나요? 어떤 다른 식 `j` 라는 것이 있을 때 구분할 수 있을까요? 물론 이죠. `i` 라는 식의 주소값은 실제 변수 `i` 의 주소값이 될 것입니다. 그렇다면 `i` 는 이동 가능한가요? 아니죠. `int&& x = i;` 는 컴파일되지 않습니다. 따라서 `i` 는 \serif{lvalue} 입니다.

**이름을 가진** 대부분의 객체들은 모두 \serif{lvalue} 입니다. 왜냐하면 해당 객체의 주소값을 취할 수 있기 때문이죠. \serif{lvalue} 카테고리 안에 들어가는 식들을 나열해보자면 ([자세한 내용은 여기를 참조!](https://en.cppreference.com/w/cpp/language/value_category))

* 변수, 함수의 이름, 어떤 타입의 데이터 멤버 (예컨대 `std::endl`, `std::cin`) 등등
* 좌측값 레퍼런스를 리턴하는 함수의 호출식. `std::cout << 1` 이나 `++it` 같은 것들
* `a = b, a += b, a *= b` 같이 복합 대입 연산자 식들
* `++a, --a` 같은 전위 증감 연산자 식들
* `a.m, p->m` 과 같이 멤버를 참조할 때. 이 때 `m` 은 `enum` 값이거나 `static` 이 아닌 멤버 함수인 경우 제외. (아래 설명 참조)
```cpp-formatted
class A {
  int f();         // static 이 아닌 멤버 함수
  static int g();  // static 인 멤버 함수
}

A a;
a.g;  // <-- lvalue
a.f;  // <-- lvalue 아님 (아래 나올 prvalue)
```

* `a[n]` 과 같은 배열 참조 식들
* **문자열 리터럴** `"hi"`

등등을 볼 수 있습니다. 특히 이 \serif{lvalue} 들은 주소값 연산자(`&`) 를 통해 해당 식의 주소값을 알아 낼 수 있습니다. 예를 들어서 `&++i` 나 `&std::endl` 은 모두 올바른 작업입니다. 또한 \serif{lvalue} 들은 좌측값 레퍼런스를 초기화 하는데에 사용할 수 있습니다.

그렇다면 한 가지 퀴즈!

```cpp-formatted
void f(int&& a) {
  a;  // <-- ?
}

f(3);
```

위 코드에서 `a` 는 무슨 값 카테고리에 들어갈까요? `a` 는 우측값 레퍼런스기는 하지만, 식 `a` 의 경우는 \serif{lvalue} 입니다! 왜냐하면 이름이 있잖아요. 식 `a` 의 **타입** 은 우측값 레퍼런스가 맞지만, 식 `a` 의 **값 카테고리** 는 \serif{lvalue} 가 됩니다. 따라서 아래 같은 식들 모두 컴파일 됩니다.

```cpp-formatted
#include <iostream>

void f(int&& a) { std::cout << &a; }
int main() { f(3); }
```

만약에 `a` 가 우측값 레퍼런스니까 `a` 는 우측값일꺼야 라고 생각했다면, 타입과 값 카테고리가 다른 개념이란 사실을 헷갈린 경우가 되겠습니다.


#### prvalue

```cpp-formatted
int f() { return 10; }

f();  // <-- ?
```

그렇다면 위 코드의 `f()` 를 살펴봅시다. 위 식은 어떤 카테고리에 들어갈까요? 먼저 `f()` 는 실체가 있을 까요? 쉽게 생각해서 `f()` 의 주소값을 취할 수 있을까요? 아닙니다. 하지만 `f()` 는 우측값 레퍼런스에 붙을 수 있습니다. 따라서 `f()` 는 \serif{prvalue} 입니다.

\serif{prvalue} 로 대표적인 것들은 아래와 같습니다.

* 문자열 리터럴을 **제외** 한 모든 리터럴들. `42, true, nullptr` 같은 애들
* 레퍼런스가 아닌 것을 리턴하는 함수의 호출식. 예를 들어서 `str.substr(1, 2), str1 + str2`
* 후위 증감 연산자 식. `a++`, `a--`
* 산술 연산자, 논리 연산자 식들. `a + b`, `a && b`, `a < b` 같은 것들을 말합니다. 물론, 이들은 연산자 오버로딩 된 경우들 말고 디폴트로 제공되는 것들을 말합니다.
* 주소값 연산자 식 `&a`
* `a.m, p->m` 과 같이 멤버를 참조할 때. 이 때 `m` 은 `enum` 값이거나 `static` 이 아닌 멤버 함수여야함.
* `this`
* `enum` 값
* 람다식 `[]() { return 0;};` 과 같은 애들.

등등... 여러가지가 있습니다.

이 \serif{prvalue} 들은 **정체를 알 수 없는 녀석들** 이기 때문에 주소값을 취할 수 없습니다. 따라서 `&a++` 이나 `&42` 와 같은 문장은 모두 오류입니다. 또한, \serif{prvalue} 들은 식의 **좌측에 올 수 없습니다**. 하지만 \serif{prvalue} 는 우측값 레퍼런스와 **상수 좌측값 레퍼런스**를 초기화 하는데 사용할 수 있습니다. 예를 들어서;

```cpp-formatted
const int& r = 42;
int&& rr = 42;
// int& rrr = 42; <-- 불가능
```

와 같이 됩니다.

#### xvalue

만일 값 카테고리가 \serif{lvalue} 와 \serif{prvalue} 두 개로만 구분된다면 문제가 있습니다. 만일 좌측값으로 분류되는 식을 이동 시킬 방법이 없기 때문입니다. 따라서 우리는 좌측값 처럼 정체가 있지만 이동도 시킬 수 있는 것들을 생각해봐야 합니다.

C++ 에서 이러한 형태의 값의 카테고리에 들어가는 식들로 가장 크게 **우측값 레퍼런스를 리턴하는 함수의 호출식** 을 들 수 있습니다. 대표적으로 `std::move(x)` 가 있지요. `std::move` 함수는 아래와 같이 생겼습니다.

```cpp-formatted
template <class T>
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept;
```

다른 복잡한 것들은 모두 건너 뛰더라도 `move` 의 리턴 타입 만큼은 우측값 레퍼런스 임을 알 수 있습니다. 따라서 `move` 를 호출한 식은 \serif{lvalue} 처럼 좌측값 레퍼런스를 초기화 하는데 사용할 수 도 있고, \serif{prvalue} 처럼 우측값 레퍼런스에 붙이거나 이동 생성자에 전달해서 이동 시킬 수 있습니다.

```lec-warning
여기에서 다룬 값 카테고리에 대한 설명은 굉장히 가볍게 짚고 넘어간 것입니다. 좀 더 자세히 알고 싶은 분들은 언제나 그렇듯이 훌륭한 정보를 제공해주는 [cppreference](https://en.cppreference.com/w/cpp/language/value_category) 와 [C++ 표준](http://eel.is/c++draft/basic.lval) 을 참조하시기를 바라겠습니다.
```

자, 그렇다면 이제 다시 `decltype` 에 대한 설명으로 돌아와보겠습니다. 앞서 말했듯이 `decltype` 에 식별자 표현식이 아닌 식이 전달된다면, 식의 타입이 `T` 라고 할 때 아래와 같은 방식으로 타입을 리턴한다고 하였습니다.

* 만일 식의 값 종류가 \serif{xvalue} 라면 `decltype` 는 `T&&` 가 됩니다.
* 만일 식의 값 종류가 \serif{lvalue} 라면 `decltype` 는 `T&` 가 됩니다.
* 만일 식의 값 종류가 \serif{prvalue} 라면 `decltype` 는 `T` 가 됩니다.

그렇다면 예를 들어서 아래의 코드를 살펴봅시다.

```cpp-formatted
int a, b;
decltype(a + b) c;  // c 의 타입은?
```

위에서 본 바에 따르면 `a + b` 는 \serif{prvalue} 이므로 `a + b` 식의 실제 타입인 `int` 로 추론됩니다. 따라서 위 식은 그냥 `int c;` 를 한 것 과 똑같게 되겠지요.

그렇다면 아래와 같은 식은 어떨까요?

```cpp-formatted
int a;
decltype((a)) b;  // b 의 타입은?
```

일단 `(a)` 는 식별자 표현식이 아니기 때문에 어느 값 카테고리에 들어가는지 생각해봐야 합니다. 쉽게 생각하면 `&(a)` 와 같이 주소값 연산자를 적용할 수 있고, 당연히도 이동 불가능 이므로 \serif{lvalue} 가 됩니다. 따라서 `b` 는 `int` 가 될 것이라는 예상과는 다르게 **`int&` 로 추론됩니다!**

이는 C++ 에서 괄호의 유무로 인해 무언가 결과가 달라지는 첫 번째 경우가 아닐까 싶네요.

### decltype 의 쓰임새

그렇다면 `decltype` 는 도대체 왜 쓰이는 것일까요? 타입 추론이 필요한 부분에는 그냥 `auto` 로도 충분하지 않을까요? 예를 들어서

```cpp-formatted
int i = 4;
auto j = i;  // int j = i;
```

를 할 때나

```cpp-formatted
int i = 4;
decltype(i) j = i;  // int j = i;
```

는 같기 때문이지요. 하지만 `auto` 는 엄밀히 말하자면 정확한 타입을 표현하지 않습니다. 예를 들어서

```cpp-formatted
const int i = 4;
auto j = i;         // int j = i;
decltype(i) k = i;  // const int k = i;
```

`auto` 의 경우 `const` 를 띄어버리지만, `decltype` 의 경우 이를 그대로 보존합니다. 그 외에도 배열의 경우 `auto` 는 암시적으로 포인터로 변환하지만, `decltype` 의 경우 배열 타입 그대로를 전달할 수 있습니다. 예컨대

```cpp-formatted
int arr[10];
auto arr2 = arr;     // int* arr2 = arr;
decltype(arr) arr3;  // int arr3[10];
```

이 되겠지요. 즉 `decltype` 를 이용하면 **타입 그대로** 정확하게 전달할 수 있습니다.

물론 이것 뿐만이 아닙니다. 템플릿 함수에서 어떤 객체의 타입이 템플릿 인자들에 의해서 결정되는 경우가 있습니다. 예를 들어서 아래와 같은 함수를 생각해봅시다.

```cpp-formatted
template <typename T, typename U>
void add(T t, U u, /* 무슨 타입이 와야 할까요? */ result) {
  *result = t + u;
}
```

위 `add` 함수는 단순히 `t` 와 `u` 를 더해서 `result` 에 저장하는 함수 입니다. 문제는 이 `result` 의 타입이 `t + u` 의 결과에 의해 결정된다는 사실 입니다. 예를 들어서 `t` 가 `double` 이고 `u`s 가 `int` 라면 `result` 의 타입은 `double*` 이 되겠지요.

따라서 이런 경우에 `result` 에 타입이 올 자리에 `decltype` 를 사용해주면 됩니다.

```cpp-formatted
template <typename T, typename U>
void add(T t, U u, decltype(t + u)* result) {
  *result = t + u;
}
```

와 같이 말이지요.

그렇다면 위 함수를 살짝 바꿔서 `result` 에 전달하는 대신에 그냥 더한 값을 리턴해버리는 함수를 만들어보면 어떨까요? 만약에 그냥

```cpp-formatted
template <typename T, typename U>
decltype(t + u) add(T t, U u) {
  return t + u;
}
```

위와 같이 한다면 한 가지 문제가 있습니다. 위 식을 컴파일 한다면 아래와 같이 오류가 발생할 것입니다.

```compiler-warning
test2.cc:3:10: error: ‘t’ was not declared in this scope
 decltype(t + u) add(T t, U, u) {
          ^
test2.cc:3:10: error: ‘t’ was not declared in this scope
test2.cc:3:14: error: ‘u’ was not declared in this scope
 decltype(t + u) add(T t, U, u) {
```

컴파일러가 위 식을 컴파일 할 때 `decltype` 안의 `t` 와 `u` 를 보고 *아니 이건 뭐지* 한 것입니다. `t` 와 `u` 의 정의가 `decltype` 나중에 나오기 때문이지요. 이 경우 **함수의 리턴값을 인자들 정의 부분 뒤에 써야 합니다**. 이는 `C++ 14` 부터 추가된 아래와 같은 문법으로 구현 가능합니다.

```cpp-formatted
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
  return t + u;
}
```

바로 리턴값 자리에는 그냥 `auto` 라고 써놓고, `->` 뒤에 함수의 실제 리턴 타입을 지정해주는 것입니다. 이는 람다 함수 문법과 매우 유사합니다.

### std::declval

다음으로 살펴볼 것은 `decltype` 처럼 `C++ 11` 에서 새로 추가된 `std::declval` 함수 입니다. `declval` 은 `decltype` 과는 다르게 키워드가 아닌 `<utilty>` 에 정의된 **함수** 입니다.

예를 들어서 어떤 타입 `T` 의 `f` 라는 함수의 리턴 타입을 정의하고 싶다고 해봅시다. 그렇다면 `decltype` 를 이용하면 아래와 같은 코드를 작성할 수 있을 것입니다.

```cpp-formatted
struct A {
  int f() { return 0; }
};

decltype(A().f()) ret_val;  // int ret_val; 이 된다.
```

참고로 위 과정에서 실제로 `A` 의 객체가 생성되거나 함수 `f` 가 호출되거나 그러지는 않습니다. `decltype` 안에 들어가는 식은, **그냥 식의 형태로만 존재할 뿐 컴파일 시에, `decltype()` 전체 식이 타입으로 변환되기 때문에 `decltype` 안에 있는 식은 런타임 시에 실행되는 것이 아닙니다**.

물론 그렇다고 해서 `decltype` 안에 문법상 틀린 식을 전달할 수 있는 것은 아닙니다. 예를 들어서 어떤 클래스에서 디폴트 생성자가 없다고 해봅시다.

```cpp-formatted
struct B {
  B(int x) {}
  int f() { return 0; }
};

int main() {
  decltype(B().f()) ret_val;  // B() 는 문법상 틀린 문장 :(
}
```

컴파일 한다면 아래와 같은 오류가 발생합니다.

```compiler-warning
test2.cc: In function ‘int main()’:
test2.cc:8:14: error: no matching function for call to ‘B::B()’
   decltype(B().f()) ret_val; // B() 는 문법상 틀린 문장 :(
              ^
test2.cc:3:3: note: candidate: B::B(int)
   B(int x) {}
   ^
test2.cc:3:3: note:   candidate expects 1 argument, 0 provided
```

그야 이유는 단순합니다.

```cpp-formatted
B()
```

`B()` 에 해당하는 생성자가 존재하지 않는 다는 것이지요. 우리는 그냥 `B` 의 멤버 함수 `f` 의 타입 참조만 필요할 뿐인데, 실제 `B` 객체를 생성할 것도 아닌데도 `B` 의 생성자 규칙에 맞는 코드를 작성해야합니다.

물론 우리는 그냥 `B(1)` 과 같이 쓰면 된다는 것을 알고 있습니다. 그런데, 아래와 같은 상황을 생각해보세요.

```cpp-formatted
template <typename T>
decltype(T().f()) call_f_and_return(T& t) {
  return t.f();
}
```

위 함수는 어떤 임의의 타입 `T` 의 객체를 받아서 해당 객체의 멤버함수 `f` 를 호출해주는 함수 입니다. 이 함수를 이용하는 객체들에 멤버 함수 `f` 가 정의되어 있다고 가정한다면, 모두 이용할 수 있습니다.

문제는 모든 타입 `T` 들이 디폴트 생성자 `T()` 를 정의하고 있지 않을 수도 있다는 말입니다.

```cpp-formatted
template <typename T>
decltype(T().f()) call_f_and_return(T& t) {
  return t.f();
}
struct A {
  int f() { return 0; }
};
struct B {
  B(int x) {}
  int f() { return 0; }
};

int main() {
  A a;
  B b(1);

  call_f_and_return(a);  // ok
  call_f_and_return(b);  // BAD
}
```

컴파일 하였다면

```compiler-warning
test2.cc: In function ‘int main()’:
test2.cc:18:22: error: no matching function for call to ‘call_f_and_return(B&)’
   call_f_and_return(b); // BAD
                      ^
test2.cc:2:19: note: candidate: template<class T> decltype (T().f()) call_f_and_return(T&)
 decltype(T().f()) call_f_and_return(T& t) {
                   ^~~~~~~~~~~~~~~~~
test2.cc:2:19: note:   template argument deduction/substitution failed:
test2.cc: In substitution of ‘template<class T> decltype (T().f()) call_f_and_return(T&) [with T = B]’:
test2.cc:18:22:   required from here
test2.cc:2:10: error: no matching function for call to ‘B::B()’
 decltype(T().f()) call_f_and_return(T& t) {
          ^~~
```

와 같이 오류가 발생합니다.

위 경우 `call_f_and_return` 함수에 `a` 를 전달했을 때에는 `A` 에 디폴트 생성자가 있으므로 잘 컴파일 되지만, `b` 를 전달할 때에는 `B` 에 디폴트 생성자가 없으므로 오류가 발생하게 됩니다.

따라서 위 처럼 직접 생성자를 사용하는 방식은 전달되는 타입들의 생성자가 모두 같은 꼴이지 않을 경우 문제가 생깁니다.

이 문제는 `std::declval` 를 사용하면 깔끔하게 해결할 수 있습니다.

```cpp-formatted
#include <utility>

template <typename T>
decltype(std::declval<T>().f()) call_f_and_return(T& t) {
  return t.f();
}
struct A {
  int f() { return 0; }
};
struct B {
  B(int x) {}
  int f() { return 0; }
};

int main() {
  A a;
  B b(1);

  call_f_and_return(a);  // ok
  call_f_and_return(b);  // ok
}
```

위 코드는 잘 컴파일 됩니다.

`std::declval` 에 타입 `T` 를 전달하면, `T` 의 생성자를 직접 호출하지 않더라도 `T` 가 생성된 객체를 나타낼 수 있습니다. 즉,

```cpp-formatted
std::declval<T>()
```

를 통해 심지어 `T` 에 생성자가 존재하지 않더라도 마치 `T()` 를 한 것과 같은 효과를 낼 수 있지요. 따라서 앞서 발생하였던 생성자의 형태가 모두 달라서 발생하는 오류를 막을 수 있습니다.

참고로 `declval` 함수를 타입 연산에서만 사용해야지, 실제로 런타임에 사용하면 오류가 발생합니다.

```cpp-formatted
#include <utility>

struct B {
  B(int x) {}
  int f() { return 0; }
};

int main() { B b = std::declval<B>(); }
```

컴파일 하였다면

```compiler-warning
/usr/include/c++/7/type_traits: In instantiation of ‘typename std::add_rvalue_reference< <template-parameter-1-1> >::type std::declval() [with _Tp = B; typename std::add_rvalue_reference< <template-parameter-1-1> >::type = B&&]’:
test2.cc:9:25:   required from here
/usr/include/c++/7/type_traits:2256:7: error: static assertion failed: declval() must not be used!
       static_assert(__declval_protector<_Tp>::__stop,
       ^~~~~~~~~~~~~
```

와 같이 오류가 발생합니다.

참고로 C++ 14 부터는 함수의 리턴 타입을 컴파일러가 알아서 유추해주는 기능이 추가되었습니다. 이 경우 그냥 함수 리턴 타입을 `auto` 로 지정해주면 됩니다.

```cpp-formatted
template <typename T>
auto call_f_and_return(T& t) {
  return t.f();
}
```

따라서 위 처럼 간단하게 쓸 수 있습니다.

물론 그렇다고 해서 `declval` 의 쓰임새가 없어지냐? 아닙니다. 다음 강좌에서 `<type_traits>` 라이브러리를 다루면서 `decltype` 과 `std::declval` 을 사용한 놀라운 템플릿 메타프로그래밍 기법들을 소개하고자 합니다.

그럼 이번 강좌는 여기에서 마치도록 하겠습니다.

```lec-summary
`decltype` 키워드를 통해서 우리가 원하는 식의 타입을 알 수 있습니다. 만일 해당 식이 단순한 식별자 표현식 (identifier expression) 이라면 그냥 그 식의 타입으로 치환됩니다. 그 이외의 경우라면 해당 식의 값 카테고리가 뭐냐에 따라서 `decltype` 의 타입이 정해집니다.
C++ 의 모든 식에는 두 가지 꼬리표가 따라다니는데 하나는 타입이고, 다른 하나는 값 카테고리 입니다. 
값 카테고리는 크게 3 가지 종류로 \serif{lvalue, prvalue, xvalue} 가 있습니다.
`std::declval` 함수를 사용해서 원하는 타입의 생성자 호출을 우회해서 멤버 함수의 타입에 접근할 수 있습니다.
```

##@ chewing-cpp-end