본 글은 코드누리의 Template Programming 강좌를 개인 학습 목적으로 정리한 글 입니다.
https://www.ecourse.co.kr/course-status/
Type Traits
- 컴파일 시간에 타입에 대한 정보, 변형된 타입을 얻을 때 사용함
- 메타 함수라고도 불림
T가 포인터인지 알아내는 테크닉
#include <iostream>
#include <string>
// foo의 T가 포인터인지 알아내려면 구조체를 만듦
template<typename T> struct xis_pointer
{
enum { value = false};
};
//부분 특수화
template<typename T> struct xis_pointer<T*>
{
enum { value = true };
};
template<typename T> void foo(T v)
{
// 부분 특수화를 통해 is_pointer 구조체의 value를 따로 정의함
if (xis_pointer<T>::value)
std::cout << "pointer" << std::endl;
else
std::cout << "not pointer" << std::endl;
}
int main()
{
int n = 3;
foo(n);
foo(&n);
}
- xis_pointer는 가만 보면 함수처럼 보이지만 실제로는 enum 상수를 꺼낸 것
- enum 상수는 컴파일 타임에 결정되므로 컴파일 타임에 사용됨, 이걸 메타 함수라고 부른다.
enum을 쓴 이유 (bool b = false 이런 식으로 쓰면 안되나?)
- 예전에는 구조체 내부에서 변수를 초기화할 수 없었다는 점
- 변수가 해석되는 것은 런타임에 해석되지만 우리는 컴파일 타임에 값을 알고 싶으므로 enum을 쓴다
- (enum은 클래스이름::으로 접근이 가능함)
C++17부터는 static constexpr을 붙여서 쓰면 동일하게 사용 가능하다.
(타입이 나와있으므로 가독성 측면에서 더 좋음)
포인터의 변형에 대응하기
#include <iostream>
#include <string>
// foo의 T가 포인터인지 알아내려면 구조체를 만듦
template<typename T> struct xis_pointer
{
enum { value = false};
};
//부분 특수화
template<typename T> struct xis_pointer<T*>
{
enum { value = true };
};
int main()
{
std::cout << xis_pointer<int>::value << std::endl;
std::cout << xis_pointer<int*>::value << std::endl;
std::cout << xis_pointer<int* const>::value << std::endl;
std::cout << xis_pointer<int* volatile>::value << std::endl;
std::cout << xis_pointer<int* const volatile>::value << std::endl;
std::cout << xis_pointer<int* volatile const>::value << std::endl;
}
0
1
0
0
0
0
- 결과값을 보면 int* const, int* volatile, int* const volatile은 부분특수화 버전으로 넘어가지 않는 것을 볼 수 있다.
- 대응되게 하려면 아래와 같이 만들어줘야 함
template<typename T> struct xis_pointer<T* const>
{
enum { value = true };
};
template<typename T> struct xis_pointer<T* volatile>
{
enum { value = true };
};
template<typename T> struct xis_pointer<T* const volatile>
{
enum { value = true };
};
배열인지 아닌지 검사하기
- 위에서 했던 포인터 버전과 비슷함
#include <iostream>
#include <string>
template<typename T> struct xis_array
{
static constexpr bool value = false;
};
//배열의 타입이 int[3]이기 때문에 일반화하면 T[N]이 되어야 한다.
template<typename T, int N> struct xis_array<T[N]>
{
static constexpr bool value = true;
};
//배열의 크기를 알 수 없을 때(넘어올 때 애초에 T[]로 넘어와야 함)
template<typename T> struct xis_array<T[]>
{
static constexpr bool value = true;
};
template<typename T>
void foo(T& a)
{
if (xis_array<T>::value)
std::cout << "array" << std::endl;
else
std::cout << "not array" << std::endl;
}
int main()
{
int x[3] = { 1, 2, 3 };
foo(x);
}
- 부분 특수화에서 인자가 더 늘어날 수 있다.
- 배열의 경우 타입을 정확히 알아야 함 (int x[3]의 타입은 int[3])
- 변수의 타입은 변수의 이름을 뺀 나머지므로..
#include <iostream>
#include <string>
template<typename T> struct xis_array
{
static constexpr bool value = false;
static constexpr size_t size = -1;
};
// 배열 여부를 알수 있는 것처럼 N을 템플릿 인자로 받으니까 배열의 크기도 뽑아낼 수 있다!
template<typename T, int N> struct xis_array<T[N]>
{
static constexpr bool value = true;
static constexpr size_t size = N;
};
template<typename T>
void foo(T& a) // 참조로 받아야 배열로 되고 값으로 받으면 포인터로 받는다
{
if (xis_array<T>::value)
{
std::cout << "배열 크기 : " << xis_array<T>::size << std::endl;
}
}
int main()
{
int x[3] = { 1, 2, 3 };
foo(x);
}
- 템플릿에서는 배열의 경우 타입과 배열 개수를 분리할 수 있으므로,
- 템플릿으로 배열을 넘기면 배열의 개수를 알아낼 수 있다.
int2type
- 함수 오버로딩은 인자의 개수가 다르거나 타입이 다를 때 동작함
#include <iostream>
#include <string>
void foo(int n) {}
void foo(double d) {}
int main()
{
foo(3);
foo(3.4);
// 아래 코드를 각각 다른 함수로 호출하는 법
foo(0);
foo(1);
}
- foo(0)과 foo(1)을 각각 다른 함수로 호출할 수 있을까?
#include <iostream>
#include <string>
// 정수를 타입으로 만드는 시스템
template<int N> struct int2type
{
enum {value = N};
};
void foo(int n) {}
int main()
{
// 아래 코드를 각각 다른 함수로 호출하는 법
foo(0);
foo(1);
int2type<0> t0;
int2type<1> t1; //t0과 t1는 템플릿으로 만들어진 다른 타입의 구조체이다.
//따라서 foo(t0), foo(t1)는 다른 함수를 호출할 수 있다.
}
- 템플릿 인자로 int N을 받는 구조체 타입을 만듦
- int2type은 구조체 타입을 만드는 틀일 뿐이고, 실제 구조체는 아니므로
- int2type을 통해 만들어지는 int2type<0>, int2type<1>은 별개의 구조체 타입으로 봐야 한다.
- 따라서 foo(int2type<0>)과 foo(int2type<1>)은 다른 함수를 호출할 수 있다!
int2type은 컴파일 시간 정수형 상수를 각각의 독립된 타입으로 만드는 도구.
int2type을 함수 오버로딩, 템플릿 인자, 상속 등에 사용할 수 있다.
int2type 활용하기
#include <iostream>
#include <string>
template<typename T> struct xis_pointer
{
static constexpr bool value = false;
};
template<typename T> struct xis_pointer<T*>
{
static constexpr bool value = true;
};
template<typename T> void printv(T v)
{
//if는 실행시간 조건문이라 compile할 때 false로 결정 됐지만 컴파일되어버림
//그럼 *v를 만나게 되고 에러가 난다
//constexpr을 사용하면 에러가 나지 않는다.
if (xis_pointer<T>::value) // if constexpr
std::cout << v << " : " << *v << std::endl;
else
std::cout << v << std::endl;
}
int main()
{
int n = 3;
printv(n);
printv(&n);
}
- 지금 상태에선 에러가 발생한다
- xis_pointer<T>::value가 컴파일 시간에 결정된다고 하더라도
- if문 자체가 실행 시간 조건문이므로 *v도 컴파일되고, 에러가 난다.
- 이 문제는 if constexpr로 해결할 수 있다.
그렇다면 조건문 안에도 템플릿을 사용한다면?
- 템플릿은 사용되지 않으면 인스턴스화되지 않는다.
- 그러면 문제 없지 않을까?
#include <iostream>
#include <string>
template<typename T>
struct xis_pointer
{
static constexpr bool value = false;
};
template<typename T>
struct xis_pointer<T*>
{
static constexpr bool value = true;
};
template<typename T> void printv_pointer(T v)
{
std::cout << v << " : " << *v << std::endl;
}
template<typename T> void printv_not_pointer(T v)
{
std::cout << v << std::endl;
}
template<typename T> void printv(T v)
{
if (xis_pointer<T>::value)
printv_pointer(v); // 템플릿은 사용해야지만 인스턴스화가 됨 - 지연된 인스턴스화
// 그러나 if문은 실행시간 컴파일인데 사용한다고 생각할지, 아닐지?
else
printv_not_pointer(v);
}
int main()
{
int n = 3;
printv(n);
printv(&n);
}
- 그러나 if문은 실행시간 조건문이라서 컴파일되고 간접 참조가 잘못되었다고 에러가 난다.
- 컴파일 시간 분기문(if constexpr)이 필요하다.
#include <iostream>
#include <string>
template<int N>
struct int2type { static constexpr int n = N; };
template<typename T>
struct xis_pointer
{
static constexpr bool value = false;
};
template<typename T>
struct xis_pointer<T*>
{
static constexpr bool value = true;
};
template<typename T> void printv_imp(T v, int2type<1>) // 포인터 타입
{
std::cout << v << " : " << *v << std::endl;
}
template<typename T> void printv_imp(T v, int2type<0>) // 포인터가 아닌 타입
{
std::cout << v << std::endl;
}
template<typename T> void printv(T v)
{
// 어느 함수를 부를지 결정하는 것은 컴파일 시간이 이루어짐
printv_imp(v, int2type<xis_pointer<T>::value>()); // 포인터면 1, 포인터가 아니면 0
// 따로 동작하게 하려면 int2type으로 감싸서 0과 1을 별개의 타입으로 구분해야 함
// int2type<1>, int2type<0>
}
int main()
{
int n = 3;
printv(n);
printv(&n);
}
- is_pointer의 결과값으로 int2type<N>을 인스턴스한다!
- 그리고 int2type<0>과 int2type<1>로 템플릿 특수화를 해놓으면 0과 1에 대해 오버로딩이 가능해진다.
integral_constant
- 위에서 만든 int2type을 C++에서 채택하여 라이브러리화한 기술
template<typename T, T N>
struct integral_constant
{
static constexpr T value = N;
};
//모든 정수 계열을 컴파일 시간 상수로 만들 수 있음
integral_constant<int, 0> t0;
integral_constant<int, 1> t1;
integral_constant<short, 0> t2;
//true와 false는 참과 거짓을 나타내는 같은 타입
//true_type과 false_type은 참/거짓을 나타내는 서로 다른 타입
typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;
//상속을 받았으니 내부에 false 값의 value가 있는거나 마찬가지
template<typename T>
struct is_pointer : false_type
{
};
template<typename T>
struct is_pointer<T*> : true_type
{
};
- 첫 번째 인자로 타입을 받고 두 번째 인자로 숫자를 받아서 인스턴스화 한다.
#include <iostream>
#include <string>
//#include <type_traits>
template<typename T, T N>
struct integral_constant
{
static constexpr T value = N;
};
typedef integral_constant<bool, true> true_type; // 그냥 bool에 true인 타입
typedef integral_constant<bool, false> false_type; // 그냥 bool에 false인 타입
template<typename T>
struct is_pointer : false_type
{
static constexpr bool value = false;
};
template<typename T>
struct is_pointer<T*> : true_type
{
static constexpr bool value = true;
};
template<typename T> void printv_imp(T v, true_type) // 포인터 타입
{
std::cout << v << " : " << *v << std::endl;
}
template<typename T> void printv_imp(T v, false_type) // 포인터가 아닌 타입
{
std::cout << v << std::endl;
}
template<typename T> void printv(T v)
{
printv_imp(v, is_pointer<T>()); //이렇게 보내면 그냥 가는 이유?
// is_pointer의 결과값이 1이면 true_type오버로딩으로 가고, 0이면 false_type 오버로딩으로 감
// is_pointer<T>()만 했는데 알맞게 넘어가는 이유
// 자식 클래스는 부모 클래스를 인자로 받는 함수로 넘어갈 수 있다.
}
int main()
{
int n = 3;
printv(n);
printv(&n);
}
- is_pointer<T>에서 아무 인자를 보내지 않아도 가는 이유에 대해 깊이 이해할 필요가 있다.
- 자식 클래스는 부모 클래스를 인자로 받는 함수로 넘어갈 수 있다는 것을 기억해야 함
정리하자면
template<typename T> void foo(T v)
{
// T가 포인터인지 조사
if (std::is_pointer<T>::value)
{
//포인터일때 동작하는 알고리즘
//그런데 여기서 *v를 쓸 수는 없음 if문은 실행시간 조건문이므로,
//if constexpr을 쓰거나 내부적으로 int2type 원리를 활용한 true_type, false_type을 이용한 오버로딩
}
else
{
}
}
type_traits의 기능
- type_traits의 기능은 크게 2가지로 볼 수 있다.
- 타입에 대한 쿼리 - is_pointer<>, is_array<> 등
- 타입에 대한 변형 - remove_pointer<>, add_pointer<>
#include <iostream>
#include <string>
//#include <type_traits>
template<typename T> struct xremove_pointer
{
//enum 상수가 아닌 타입이 필요함
typedef T type;
};
template<typename T> struct xremove_pointer<T*>
{
// T*에서 T의 type이므로 포인터가 제거된다
// 타입과 포인터를 분리하고 싶다면 이렇게 사용
typedef T type;
};
template<typename T> void foo(T v)
{
bool b = std::is_pointer<T>::value;
//포인터가 제거된 타입을 반환
typename xremove_pointer<T>::type t;
std::cout << typeid(t).name() << std::endl;
}
int main()
{
int n = 10;
foo(n);
foo(&n); // 포인터가 제거된 타입이 출력됨
}
- 포인터를 제거하는 remove_pointer
- T*에 대한 특수화를 만들면 거기서 T는 타입이 되는 원리를 이용함
- 그렇다면 <T*****>는 어떻게 처리할까?
remove_all_pointer
- T*****라도 결국 T의 포인터 타입이므로 T* 특수화 버전에 들어간다.
- 그럼 아래와 같이 만들게 되면 자신을 재귀 호출하면서 결국 포인터를 제거한 타입이 된다.
template<typename T> struct xremove_all_pointer
{
typedef T type;
};
template<typename T> struct xremove_all_pointer<T*>
{
typedef typename xremove_all_pointer<T>::type type;
△
// xremove_all_pointer<int****>::type ┌ㅡㅡㅡㅡ┘
// xremove_all_pointer<int***>::type /
// xremove_all_pointer<int**>::type /
// xremove_all_pointer<int*>::type type
};
int main()
{
xremove_pointer<int**>::type n; //포인터가 2개 이상이라면 어떻게?
std::cout << typeid(n).name() << std::endl;
xremove_all_pointer<int*****>::type an;
std::cout << typeid(an).name() << std::endl;
}
int*
int
함수의 정보를 구하는 Traits
- primary_template를 만들고 typename T type을 제공
- 부분 특수화를 통해 타입을 받는 특수화 버전을 만들고 얻고 싶은 템플릿 인자를 type으로 typedef 한다.
- 함수타입 double(short, int) -> R(A1, A2)
#include <iostream>
#include <string>
#include <Windows.h>
//#include <type_traits>
using namespace std;
double hoo(short a, int b) { return 0; }
template<typename T> struct result_type
{
//인자가 잘못 전달됐다면? 에러 발생시키는 방법
// 꼭 TYPEDEF가 존재할 필요는 없음
//typedef T type;
};
// 함수의 파라미터 타입과 리턴 타입을 구하는 방법
template<typename R, typename A1, typename A2>
struct result_type<R(A1, A2)>
{
typedef R type;
};
template<typename T>
void foo(T& t)
{
typename result_type<T>::type ret;
std::cout << typeid(ret).name() << std::endl;
//여기서 함수의 반환 타입만 뽑아내고 싶다면?
}
int main()
{
foo(hoo);
}
- 인자 2개를 받는 함수를 템플릿에 넘기면 그걸 받는 특수화 result_type<R(A1, A2)>로 들어간다.
- 그럼 템플릿 안에서 type을 재정의할 때 R로 할것인지 A1, A2로 할 것인지는 내 마음임
- 다만 A1, A2는 인자가 2개니까 2개를 모두 처리하려면 몇 번째 인자인지는 사용자 입력으로 받아야 함(아래서 구현)
- primary template에서 type을 구현하지 않으면 인자를 2개 받는 함수타입이 넘어오지 않으면 에러가 뜨도록 만들어짐
- 이것도 만드는 사람 마음
매개변수 타입 구하기
- 매개변수는 1개 이상일 수 있으니 특수화할 때 번호랑 같이 받도록 해야 함
- n개의 매개변수를 받는 함수도 받도록 일반화시킨다면 가변인자 템플릿을 써야 함
#include <iostream>
//#include <type_traits>
using namespace std;
double hoo(short a, int b) { return 0; }
template<typename T, size_t N>
struct argument_type
{
typedef T type;
};
//특정 매개변수의 타입을 추출해내기 위해 0과 1같은 번호와 같이 특수화
template<typename R, typename A1, typename A2>
struct argument_type<R(A1, A2), 0>
{
typedef A1 type;
};
template<typename R, typename A1, typename A2>
struct argument_type<R(A1, A2), 1>
{
typedef A2 type;
};
template<typename T>
void foo(T& t)
{
typename argument_type<T, 0>::type ret;
std::cout << typeid(ret).name() << std::endl;
//여기서 함수의 반환 타입만 뽑아내고 싶다면?
}
int main()
{
foo(hoo);
}
- 함수 리턴타입 구하는 C++ 표준 함수는 아래와 같다.
- result_of (C++17 이전)
- invoke_result(C++17 이후)
'C++ > [ecourse] C++ Template' 카테고리의 다른 글
9. 가변인자 템플릿-2 (0) | 2022.10.07 |
---|---|
8. 가변인자 템플릿 (0) | 2022.10.06 |
6. 템플릿 특수화 (1) | 2022.09.29 |
5. 템플릿 기본 문법 - typename, template (0) | 2022.09.28 |
4. 템플릿 기본 문법 - 클래스 템플릿 (0) | 2022.09.27 |
댓글