C++ 템플릿이 타입을 어떻게 추론하는지 그 법칙을 알아보자.
1. 템플릿
일단, 템플릿이 뭐지? 라는 분을 위해 간략히 설명 하자면,
템플릿은 Generic 프로그래밍을 위해 C++ 이 제공하는 장치로, 변수 타입과 무관하게 클래스 및 함수를 정의 가능하게 해주는 방법이다. (음 뭔말이지?)
예를 들면, 아래와 같은 타입 별 교환 (swap) 함수를,
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
void swap(double& a, double& b) {
double tmp = a;
a = b;
b = tmp;
}
void swap(CustomType& a, CustomType& b) {
CustomType tmp = a;
a = b;
b = tmp;
}
아래와 같은 템플릿 선언을 통해 타입과 무관하게 구현이 가능하다는 뜻이다.
template <typename T>
void swap(T& a, T& b) {
T tmp;
tmp = a;
a = b;
b = tmp;
}
결국, swap을 호출 시 사용한 인자에 따라 파라미터 타입을 추론하게 되는데,
예를 들어, 아래와 같이 호출하는 코드가 있으면,
int a = 1;
int b = 2;
swap(a, b);
파라미터 타입이 Int&로 추론되어 아래 함수가 선언/정의된다.
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
2. 템플릿 타입 추론
지금부터 템플릿의 파라미터 타입이 어떻게 추론되는지 그 룰을 간략하게 정리해보자.
참고로, 본 포스팅에서 아래 포스팅의 type_name() 함수를 활용할 예정이니 참고하기 바란다.
Tip: 변수의 타입 출력하기
C++ 에서는 템플릿, auto 등을 통해 타입추론이 많이 사용되므로 실제 추론된 타입이 무엇인지 궁금할떄가 많다. 아래 스택오버플로우에서 관련 마음에 드는 방법을 찾아서 공유하고자 한다. (C++11
drogrammer.tistory.com
용어
파라미터 (Paramter) : 함수 선언 시 인자로 받을 변수를 의미 ex. int func(int a, int b)
인자 (Argument) : 함수 호출 시 실제로 파라미터로 사용하는 인자 ex. func(mya, myb)
2.1. 파라미터 타입이 레퍼런스일 경우
T& 이 파라미터 타입일 경우, 당연하게도 주어진 타입의 레퍼런스로 추론한다. ㅎ;; 단, 주어진 인자가 이미 레퍼런스일 경우에는 레퍼런스를 한번 더 붙이지 않는다. (즉, int 는 int& 으로 추론되고, int& 은 int&으로 추론된다.)
template <typename T>
void r(T& a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
int i = 1;
int& ir = i;
const int ci = 1;
const int& cir = ci;
r(i); // int& 으로 추론
r(ir); // int& 으로 추론
r(ci); // int const& 으로 추론
r(cir); // int const& 으로 추론
2.2. 파라미터 타입이 상수 레퍼런스일 경우
const T& 이 파라미터 타입일 경우, 2.1.1 과 방식은 동일하지만 추론된 결과에 const 가 붙는다. (당연한걸 왜 설명하고 있지..)
template <typename T>
void cr(const T& a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
int i = 1;
int& ir = i;
const int ci = 1;
const int& cir = ci;
cr(i); // int const& 로 추론
cr(ir); // int const& 로 추론
cr(cir); // int const& 로 추론
cr(cir2); // int const& 로 추론
2.3. 파라미터 타입이 포인터일 경우
T* 이 파라미터 타입일 경우, 포인터만 인자로 받을 수 있으므로 주어진 타입을 그대로 추론한다. (참고로, 레퍼런스는 일반타입과 레퍼런스 타입을 모두 받을 수 있었고 둘다 레퍼런스로 추론되었었다.)
template <typename T>
void p(T* a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
int i = 1;
const int* pi = &i;
p(&i); // int* 로 추론
p(pi); // int const* 로 추론
2.4. 파라미터 타입이 universal 레퍼런스일 경우
Universal 레퍼런스인 T&& 이 파라미터 타입일 경우,
- 인자가 변수 같은 lvalue 면 2.1 과 동일하게 추론된다.
- 인자가 리터럴, 식 등의 rvalue 면 universal 레퍼런스로 추론된다.
template <typename T>
void ur(T&& a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
int i = 1;
int& ir = i;
const int ci = 1;
const int& cir = ci;
ur(i); // int& 로 추론 (lvalue)
ur(ir); // int& 로 추론 (lvalue)
ur(ci); // int const& 로 추론 (lvalue)
ur(cir); // int const& 로 추론 (lvalue)
ur(10); // int&& 로 추론 (rvalue)
ur(i + ir); // int&& 로 추론 (rvalue)
2.5. 파라미터 타입이 value 타입일 경우
T 가 파라미터 타입일 경우, 인자의 레퍼런스, 상수(const), volatile 특성은 제거하고 추론한다.
template <typename T>
void v(T a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
int i = 1;
int& ir = i;
const int ci = 1;
const int& cir = ci;
v(i); // int 로 추론 (lvalue)
v(ir); // int 로 추론 (lvalue)
v(ci); // int 로 추론 (lvalue)
v(cir); // int 로 추론 (lvalue)
v(10); // int 로 추론 (rvalue)
v(i + ir); // int 로 추론 (rvalue)
2.6. 그 외 특이한 케이스
2.6.1. 배열 (array) 추론
템플릿으로 전달되는 인자가 배열인 경우에는 value 추론 (T) 과 레퍼런스 추론 (T&) 결과가 다르다. Value 추론은 배열을 포인터로 추론해 내지만, 레퍼런스로 추론하면 고정크기 배열의 레퍼런스로 추론해 낸다.
template <typename T>
void v(T a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
template <typename T>
void r(T& a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
int ia[] = {1, 2, 3, 4, 5, 6};
v(ia); // int* 로 추론
r(ia); // int [6]& 로 추론
2.6.2. 함수 포인터의 경우
템플릿으로 전달되는 인자가 함수일 경우에는 2.6.1. 배열 추론과 유사하게 value 추론 (T) 과 레퍼런스 추론 (T&) 결과가 다르다. Value 추론은 함수 포인터로 추론해 내지만, 레퍼런스 추론은 함수 레퍼런스로 추론해 낸다.
template <typename T>
void v(T a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
template <typename T>
void r(T& a) {
std::cout << type_name<decltype(a)>() << std::endl;
}
// 추론 대상 함수
void func(int, double) {}
v(func); // void (*)(int, double) 로 추론
r(func); // void (int, double)& 로 추론
댓글