SFINAE 기법을 사용하면, 새로운 분기가 추가되면 기존 함수들의 템플릿 명세를 수정해야 합니다.
꼬리표 분배 방법은 템플릿 명세로 구분하는 것을, 함수의 인자로 구분하여 사용하는 방법입니다.
기존 함수들의 수정이 필요 없고, 깔끔해 지는 대신 표면에 드러난 함수와 처리하는 함수가 나눠지게 됩니다.
드러난 함수는 타입에 따라 알맞은 꼬리표를 실제 처리하는 함수에 전달해 주고, 처리하는 함수는 꼬리표에 따라 오버로딩해서 사용하는 방법입니다.
전달하는 방식은 템플릿 특수화(template specialization) 혹은 객체 넘겨 주기가 있습니다.
다만 특수화 해야 할 타입이 많고, 기능이 겹친다면 객체에 의한 꼬리표 분배 방식이 더 선호됩니다.
전자를 tag dispatching by type, 후자를 tag dispatching by instance 라고 합니다.
실제로 STL 및 오픈 소스 라이브러리 등에서 많이 쓰이는 기법이 객체에 의한 꼬리표 분배 방식입니다.
그럼 예시를 한번 보겠습니다.
철수가 프로그래밍을 하는데, 좀 generic 하게 짜기 위해서 템플릿 프로그래밍을 이용해 어떤 두 값을 받으면 그 두 값을 곱한 값을 돌려주는 함수를 짰습니다.
template<typename T1, typename T2>
auto mul(T1 v1, T2 v2){
return v1 * v2;
}
이때, 뒤에서 영희가 다가오더니,
"첫번째 인자가 정수인지, 실수인지 출력하게 해줘"
라고 말했습니다.
철수는 첫번째 인자를 일일이 템플릿 부분 특수화(template partial specialization) 하거나, 템플릿화를 포기하지 않고 SFINAE 를 이용하기로 하였습니다.
template<typename T1, typename T2,
std::enable_if_t<std::is_floating_point<T1>::value, int> = 0>
auto mul(T1 v1, T2 v2){
std::cout << v1 << "is floating point!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2,
std::enable_if_t<std::is_integral<T1>::value, int> = 0>
auto mul(T1 v1, T2 v2){
std::cout << v1 << "is integral!" << std::endl;
return v1 * v2;
}
이때, 영희가 다시 다가오더니,
"첫번째 인자가 long double 이거나 long 인 경우는 묶어서 저 둘과 따로 분리해줘"
라고 말했습니다.
철수는 왜 진작 말하지 않았냐며 속으로 욕을 하고는, SFINAE는 오로지 하나의 오버로드된 함수만 문법 오류(syntax failure)이 나지 않아야 하기 때문에 기존 함수들도 수정하여 결국 완성을 했습니다.
template<typename T1, typename T2,
std::enable_if_t<
std::is_floating_point<T1>::value &&
!std::is_same<T1, long double>::value, int> = 0>
auto mul(T1 v1, T2 v2){
std::cout << v1 << "is floating point!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2,
std::enable_if_t<
std::is_integral<T1>::value &&
!std::is_same<T1, long int>::value, int> = 0>
auto mul(T1 v1, T2 v2){
std::cout << v1 << "is integral!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2,
std::enable_if_t<
std::is_same<T1, long int>::value ||
std::is_same<T1, long double>::value, int> = 0>
auto mul(T1 v1, T2 v2){
std::cout << v1 << "is long ?" << std::endl;
return v1 * v2;
}
SFINAE 가 유용하긴 하지만, 경우가 추가되면 기능이 바뀌지 않는 기존의 함수를 수정해야 하고, 코드가 점점 더 더러워 지게 됩니다.
이러한 상황에서 철수를 구원해 줄 수 있는 방법이 꼬리표 분배(tag dispatching) 기법입니다.
타입에 따라 꼬리표를 분배하는 기능이 추가되어야 하고, 함수의 호출 깊이가 1칸 증가하게 됩니다.
먼저 예시부터 보겠습니다.
struct integral_type{};
struct float_type{};
struct long_type{};
template<typename T>
using what_type = std::conditional_t<
std::is_same<T, long>::value || std::is_same<T, long double>::value,
long_type,
std::conditional_t<std::is_integral<T>::value,
integral_type,
float_type>
>;
template<typename T1, typename T2>
auto mul_impl(T1 v1, T2 v2, long_type){
std::cout << v1 << "is long type!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2>
auto mul_impl(T1 v1, T2 v2, integral_type){
std::cout << v1 << "is integral type!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2>
auto mul_impl(T1 v1, T2 v2, float_type){
std::cout << v1 << "is floating point type!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2>
auto mul(T1 v1, T2 v2){
return mul_impl(v1, v2, what_type<T1>());
}
mul 함수를 호출하게 되면, what_type를 통해서 미리 정의된 꼬리표를 받게 되고, 이를 객체화 시켜 오버로드된 mul_impl 함수로 넘겨주게 됩니다.
mul_impl 함수의 꼬리표 인자는 넘길때 객체화 시켜 넘겨준다는것은 명심 하셔야 합니다.
이제 새로운 경우가 추가되면, what_type 의 명세만 수정하고 새로운 mul_impl을 오버로드 하면 됩니다.
예시에서는 템플릿 프로그래밍이 필요하여 꼬리표를 받을 때 템플릿 프로그래밍을 이용하였지만, 템플릿이 필요 없는 경우에도 꼬리표 분배 기법은 얼마든지 사용 가능합니다.
예시에서는 using 및 std::conditional 을 사용했지만, 구조체나 상속된 꼬리표를 사용해도 무방합니다.
실제로 표준 라이브러리의 std::iterator_traits 의 iterator_category 가 꼬리표 분배 방식을 사용하여 구현되어 있습니다.
en.cppreference.com/w/cpp/iterator/iterator_traits
OpenCV 도 이 기법을 이용하여 구현되어 있습니다.
(템플릿을 이용해 꼬리표를 받아오지는 않고, 최적화를 위해 조건에 따라 다른 생성자 구현을 위해 꼬리표 기법을 사용하고 있습니다.)
github.com/opencv/opencv/blob/3.4/modules/core/include/opencv2/core/matx.hpp#L214-L220
경우가 2가지 밖에 없다면, 표준 STL 타입인 std::true_type 및 std::false_type 를 사용하시면 됩니다.
STL 라이브러리의 모든 타입 비교 컨테이너가 두 꼬리표를 상속하기 때문에 이와 같이 간단하게 가능합니다.
template<typename T1, typename T2>
auto mul_impl(T1 v1, T2 v2, std::true_type){
std::cout << v1 << "is floating point type!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2>
auto mul_impl(T1 v1, T2 v2, std::false_type){
std::cout << v1 << "is integral type!" << std::endl;
return v1 * v2;
}
template<typename T1, typename T2>
auto mul(T1 v1, T2 v2){
return mul_impl(v1, v2, std::is_floating_point<T1>());
}
이해가 되셨나요?
궁금하신 점이나 피드백은 댓글로 남겨주세요!
'C++ > 템플릿(Template)' 카테고리의 다른 글
C++ 템플릿(Template): SFINAE (0) | 2020.11.01 |
---|---|
C++ 템플릿(Template): CRTP (2) | 2020.10.28 |