Type Deduction
- “Effective Modern C++” 정리
1. 템플릿 연역 규칙
// template function
template <typename T>
void f(ParamType param);
// call f function with expr
f(expr);
- 컴파일러는 컴파일도중 expr 을 이용해서 T, ParamType 에 대한 형식을 연역한다.
- T에 대해 연역된 형식은 expr 과 ParamType 의 형태에도 의존한다.
- ParamType 의 형태는 세 가지 경우가 있다.
a. ParamType이 포인터 또는 참조 형식(보편 참조 아님) 인 경우
b. ParamType이 보편 참조인 경우
c. ParamType이 포인터도 아니고 참조도 아닌 경우 - a. ParamType 이 포인터 또는 참조 형식(보편 참조 아님) 인 경우
template <typename T>
void f(T& param); // param 은 참조 형식
int x = 27; // x -> int
const int cx = x; // cx -> const int
const int& rx = x; // rx -> const int 인 x 에 대한 참조
f(x); // T -> int, ParamType -> int&
f(cx); // T -> const int, ParamType -> const int&
f(rx); // T -> const int, ParamType -> const int&
template <typename T>
void f(const T& param); // param 은 const 에 대한 참조
int x = 27; // x -> int
const int cx = x; // cx -> const int
const int& rx = x; // rx -> const int 인 x 에 대한 참조
f(x); // T -> int, ParamType -> const int&
f(cx); // T -> int, ParamType -> const int&
f(rx); // T -> int, ParamType -> const int&
template <typename T>
void f(T* param); // param 은 포인터
int x = 27; // x -> int
const int* px = &x; // px -> const int 로서 x를 가리키는 포인터
f(&x); // T -> int, ParamType -> int*
f(px); // T -> const int, ParamType -> const int*
- b. ParamType 이 보편 참조인 경우
expr이 왼값이면 T와 ParamType 둘 다 왼값으로 연역된다.
expr이 오른값이면 정상적으로 처리된다.
template <typename T>
void f(T&& param);
int x = 27; // x -> int
const int cx = x; // cx -> const int
const int& rx = x; // rx -> const int 인 x 에 대한 참조
f(x); // T -> int&, ParamType -> int&
f(cx); // T -> const int&, ParamType -> const int&
f(rx); // T -> const int&, ParamType -> const int&
f(27); // T -> int, ParamType -> int&&
- c. ParamType 이 포인터도 아니고 참조도 아닌 경우
expr형식이 참조이면 참조부분은 무시된다.
expr의 참조성을 무시한 후, 만일 expr 이 const 이면 그 const 역시 무시된다.
만일 volatile 이면 그것도 무시한다.
template <typename T>
void f(T param);
int x = 27; // x -> int
const int cx = x; // cx -> const int
const int& rx = x; // rx -> const int 인 x 에 대한 참조
f(x); // T -> int, ParamType -> int
f(cx); // T -> int, ParamType -> int
f(rx); // T -> int, ParamType -> int
template <typename T>
void f(T param);
const char* const ptr = "Fun with pointers";
f(ptr); // T -> const char*, ParamType -> const char*
- 배열을 넘기는 경우
배열은 포인터로 붕괴(decay)된다.
함수에 값으로 전달되는 배열의 형식은 포인터 형식으로 연역된다.
const char name[] = "J. P. Briggs";
const char* ptrToName = name; // 배열이 포인터로 붕괴된다.
template <typename T>
void f(T param);
f(name); // T -> const char*, ParamType -> const char*
- 배열에 대한 참조를 넘기는 경우
template <typename T>
void f(T& param);
f(name); // T -> const char name[13], ParamType -> const char (&)[13]
// 배열의 크기를 컴파일 시점 상수로 돌려주는 템플릿 함수
tempalte <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
- 함수 인수
배열과 마찬가지로 함수 형식도 함수 포인터로 붕괴할 수 있다.
void someFunc(int, double);
template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
f1(someFunc); // T -> void (*)(int, double), ParamType -> void (*)(int, double)
f2(someFunc); // T -> void (&)(int, double), ParamType -> void (&)(int, double)
2. auto 형식 연역 규칙
- “1. 템플릿 연역 규칙” 에서 T 에 해당되는 것이 auto 이며
변수 형식 지정자(type specifier)는 ParamType 과 동일한 역할을 한다.
auto x = 27; // 형식 지정자 -> auto
const auto cx = x; // 형식 지정자 -> const auto
const auto& rx = x; // 형식 지정자 -> const auto&
- auto 도 템플릿 연역 규칙과 마찬가지로 3가지 경우로 나눌 수 있다.
a. 형식 지정자가 포인터나 참조 형식(보편 참조 아님)인 경우 b. 형식 지정자가 보편 참조인 경우
c. 형식 지정자가 포인터도 아니고 참조도 아닌 경우
auto x = 27; // (case c) x -> int
const auto cx = x; // (case c) cx -> const int
const auto& rx = x; // (case a) rx -> const int&
auto&& uref1 = x; // (case b) uref1 -> int&
auto&& uref2 = cx; // (case b) uref2 -> const int&
auto&& uref3 = 27; // (case b) uref3 -> int&&
- 배열과 함수명의 붕괴 템플릿 연역 규칙과 동일하다.
const char name[] = "R. N. Briggs";
auto arr1 = name; // arr1 -> const char*
auto& arr2 = name; // arr2 -> const char (&)[13]
void someFunc(int, double);
auto func1 = someFunc; // func1 -> void (*)(int, double)
auto& func1 = someFunc; // func2 -> void (&)(int, double)
- 템플릿 연력 규칙과 다른 점
auto로 선언된 변수를 중괄호 초기치로 초기화 하는 경우 std::initializer_list로 연역된다.
auto x1 = 27; // 형식은 int, 값은 27
auto x2(27); // 형식은 int, 값은 27
auto x3 = { 27 }; // 형식은 std::initializer_list<int>, 값은 27
auto x4{ 27 }; // 형식은 int, 값은 27
auto x = { 11, 13, 9 }; // x -> std::initializer_list<int>
template <typename T>
void f(T param);
f({ 11, 23, 9 }); // 오류
- 추가 연역 규칙 (C++14)
함수 반환 형식과 람다 매개변수 선언도 가능하다.
이 연역은 템플릿 형식 연역 규칙들이 적용된다.
따라서 중괄호 초기치를 돌려주는 함수의 반환시에는 컴파일 오류가 발생된다.
// C++14
auto createInitList()
{
return { 1, 2, 3 }; // 오류
}
std::vector<int> v;
auto resetV = [&v](const auto& newValue) {
v = newValue;
};
resetV({ 1, 2, 3 }); // 오류 { 1, 2, 3 } 의 형식을 연역할 수 없음
3. decltype
- 템플릿과 auto 의 형식 연역과 다르게 이름이나 표현식의 구체적인 형식을 그대로 말해준다.
const int i = 0; // decltype(i) -> const int
bool f(const Widget& w); // decltype(w) -> const Widget&
// decltype(f) -> bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) -> int
}; // decltype(Point::y) -> int
Widget w; // decltype(w) -> Widget
if (f(w)) ... // decltype(f(w)) -> bool
template <typename T>
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v; // decltype(v) -> vector<int>
...
if (v[0] == 0) ... // decltype(v[0]) -> int&
- C++11에서 decltype은 함수의 반환 형식이 그 매개변수 형식들에 의존하는 함수 템플릿을 선언할 때 주로 쓰인다.
// C++11
template <typename Container, typename Index>
auto authAndAccess(Container c, Index i) -> decltype(c[i])
{
authenticateUser();
return c[i];
}
// 함수 앞의 auto는 후행 반환 형식(trailing return type) 구문이 쓰임을 나타낸다.
- C++11은 람다 함수가 한 문장으로 이루어져 있다면 그 반환 형식의 연역을 허용한다.
- C++14는 모든 람다와 모든 함수의 반환 형식 연역을 허용한다.
따라서 C++14 에서는 -> decltype 없이 auto 만 남겨 두어도 된다. (되기는 한다.)
// C++14
template <typename Container, typename Index>
auto authAndAccess(Container& c, Index& i)
{
authenticateUser();
return c[i];
}
std::deque<int> d;
authAndAccess(d, 5) = 10;
// auto가 지정되면 템플릿 형식 연역을 적용한다.
// auto 반환 형식 연역 과정에서 참조가 제거 되므로 반환 형식은 int가 된다.
// 따라서 이 코드는 오른값 int에 10을 재정한다.
// 따라서 컴파일 오류
- 위의 문제를 위해 decltype(auto)가 만들어졌다.
// C++14
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index& i)
{
authenticateUser();
return c[i];
}
- 변수 선언시에도 초기화 표현식에 decltype 타입 연역 규칙들을 적용할 수 있다.
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // myWidget1 -> Widget
decltype(auto) myWidget2 = cw; // mywidget2 -> const Widget&
- 위의 authAndAccess의 리턴 값을 오른쪽 값, 왼쪽 참조 모두 올 수 있게 하려면
// C++14
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
autoenticateUser();
return std::forward<Container>(c)[i];
}
// C++11
template <typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
- decltype 추가 설명
decltype을 이름에 적용하면 그 이름에 대해 선언된 형식이 산출된다.
이름이 아니고 형식이 T인 어떤 왼값 표현식에 대해서는 T& 를 보고한다.
int x = 0;
decltype(x) y = 0 // decltype(x) -> int
decltype((x)) z = x // decltype((x)) -> int&
decltype(auto) f1()
{
int x = 0;
...
return x; // decltype(x)는 int이므로 f1은 int를 반환
}
decltype(auto) f2()
{
int x = 0;
...
return (x); // decltype((x))는 int&이므로 f2는 int&를 반환
}
4. 연역된 형식 파악하는 방법
-
IDE 편집기
프로그램 개체(변수, 매개변수, 함수 등) 위에 마우스 커서를 올리면 그 개체의 형식을 표시 -
컴파일러의 진단 메시지
정의 없이 선언만 해둔 클래스 템플릿을 사용하여 오류를 유발한 후 오류 메시지에 나온 타입을 확인한다.
template <typename T>
class TD;
TD<decltype(x)> xType;
TD<decltype(y)> yType;
// class TD에 대한 정의가 없으므로 다음과 같은 오류가 발생된다.
error: 'xType'은 정의되지 않은 class 'TD<int>'를 사용합니다.
error: 'yType'은 정의되지 않은 class 'TD<const int*>'를 사용합니다.
- 실행 시점 출력-1
다음과 같은 형식으로 코드를 작성한다.
std::cout << typeid(x).name() << '\n';
std::cout << typeid(y).name() << '\n';
// int x, int* y 로 정의된 경우
// gcc, clang 의 경우
// x -> i 로 표기 (i -> integer)
// y -> PKi 로 표기 (PK -> Pointer to const)
// MS C++ 의 경우
// x -> int
// y -> int const *
- 실행 시점 출력-2
다음과 같은 경우 생각대로 나오지 않는다.
std::vector<Widget> createVec();
const auto vw = createVec();
if (!vw.empty()) {
f(&vw[0]);
}
template <typename T>
void f(const T& param)
{
std::cout << "T = " << typeid(T).name() << '\n';
std::cout << "param = " << typeid(param).name() << '\n';
}
// gcc, clang 의 경우
// T = PK6Widget
// param = PK6Widget
// MS C++ 의 경우
// T = class Widget const *
// param = class Widget const *
// 원하는 값은
// T = Widget const *
// param = Widget const * const &
// 표준에 따르면
// std::type_info::name은 반드시 주어진 형식을 마치
// 템플릿 함수에 값 전달 매개 변수로서 전달된 것처럼 취급해야 한다.
// -> 값 전달이므로 const, volatile, 참조는 빠지게 된다.
// 따라서
// T = Widget const * -> Widget const *
// param = Widget const * const & -> Widget const *
- Boost TypeIndex 라이브러리 사용
#include <boost/type_index.hpp>
template <typename T>
void f(const T& param)
{
using std::cout;
using boost::typeidex::type_id_with_cvr;
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}
std::vector<Widget> createVec();
const auto vw = createVec();
if (!vw.empty()) {
f(&vw[0]);
}
// gcc, clang
// T = Widget const *
// param = Widget const * const &
// MS C++
// T = class Widget const *
// param = class Widget const * const &
- 그래도 이런 것들은 도구일 뿐이므로 형식 연역 규칙들을 잘 숙지하도록 하자.
댓글남기기