본문 바로가기
C++/[ecourse] C++ Template

7. 템플릿 Type Traits

by 헛둘이 2022. 9. 30.

본 글은 코드누리의 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가지로 볼 수 있다.
  1. 타입에 대한 쿼리 - is_pointer<>, is_array<> 등
  2. 타입에 대한 변형 - 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 이후)

 

댓글