std::enalbe_if 클래스 템플릿은 (멤버) 함수 호출시 overload resolution 과정에 사용되는 overload resolution set에 제약을 가하는 데 사용한다. 또한 클래스 템플릿 특수화(class template specialization)를 컴파일러가 선택하는 과정에서도 class template specialization set에 제약을 가하는 도구로써 사용한다. 적절히 사용할 경우 유용하게 사용될 수 있지만 그렇지 못한 사용은 오히려 혼란을 가중시킬 수 있을 것 같다. 직접 사용할 일이 많지는 않을 것 같으나 개념은 알고 있어야할 것 같아서 적어본다. 하단의 '참고' 부분의 MSDN쪽의 링크글내에 설명된 가이드라인 참고할 것.
enable_if를 제대로 이해하기 위해서는 SFINAE(Substitution Failure Is Not An Error) 개념을 이해해야한다. enable_if는 SFINAE 사용의 한 예이다. 하단의 '참고' 부분의 Wikipedia 링크의 글이 간결하게 잘 설명하고 있다.
SFINAE를 간단하게 설명하면 **'template관련 코드에서 특정 부분만을 보면 잘못된(ill-formed) 컴파일이 실패하는 코드이나, 대체 방안이 있을 경우 문제된 코드부분은 무시되고 정상적으로 컴파일된다.'**라고 할 수도 있겠다.
enable_if의 적절한 활용예제를 생각해내기 쉽지 않았다. 작위적으로라도 (실제 유용성은 그다지 없어보이는) 예제를 만들어 보겠다.
Function template에서 사용
아래는 주어진 컨테이너의 iterator 카테고리에 따라서 컨테이너 내의 중간 위치에 있는 값을 리턴해주는 함수를 구현한 것이다.
template <
typename Container,
typename = std::enable_if_t<
std::is_same<
std::random_access_iterator_tag,
typename std::iterator_traits<typename Container::iterator>::iterator_category
>::value
>
>
auto medianValue(Container const& c)
{
return c[c.size() / 2];
}
template <
typename Container,
typename = void,
typename = std::enable_if_t<
std::is_base_of<
std::input_iterator_tag,
typename std::iterator_traits<typename Container::iterator>::iterator_category
>::value
&& !std::is_same<
std::random_access_iterator_tag,
typename std::iterator_traits<typename Container::iterator>::iterator_category
>::value
>
>
auto medianValue(Container const& c)
{
auto iter = c.begin();
std::advance(iter, c.size() / 2);
return *iter;
}
// ...
using std::vector;
using std::list;
using std::cout;
vector<int> v = { 1, 2, 3, 4, 5 };
auto vRetVal = medianValue(v);
cout << "vRetVal: " << vRetVal << std::endl; // vRetVal: 3
list<char> l = { 'a', 'b', 'c', 'd', 'e' };
auto lRetVal = medianValue(l);
cout << "lRetVal: " << lRetVal << std::endl; // lRetVal: c
여기서 두 함수의 마지막 template parameter의 default argument 표현식은 주어진 컨테이너에 대해서 둘 중에 한곳에서만 유효한 표현식이 된다. 결과적으로 하나의 함수만을 overload resolution set에 포함시키게 된다. 예를 들어서 std::vector의 인스턴스가 인자로 넘어온다고 할 경우 두 함수의 signature는 다음과 같다고 볼 수 있다.
// first function
template <
typename Container,
typename = void
>
auto medianValue(Container const& c)
...
// second function
template <
typename Container,
typename =
>
auto medianValue(Container const& c)
...
두번째 함수 signature는 잘못된(ill-formed) C++ 코드이다. 해서 해당 코드 부분은 무시된다. 대안방법에 해당하는 첫번째 함수가 존재하기 때문에 정상적으로 컴파일된다. 만약 첫번째 함수마저도 없었다면 호출가능한 함수가 없다는 형태의 컴파일 오류가 발생할 것이다.
추가로 한가지 짚고 넘어가 볼만한 부분은 두번째 overload된 함수의 signature 부분이다. 이부분을 다음과 같이 두번째 template parameter를 제거하고 작성했다면 컴파일 오류가 발생한다.
overload된 2개의 medianValue function template의 두번째 template parameter의 default argument에서 redefination관련 내용의 컴파일 오류가 발생한다. function template의 default template argument는 function template의 signature에 포함되지 않는다. 여기서는 이를 해결하기 위해서 사용하지 않는 dummy template parameter를 넣어 주었던 것이다.
위 표현은 RandomAccessIterator를 제공하지 않는 컨테이너에 대해서 다음과 같은 형태로 내부적으로 바뀔 것이고 역시 유효한(well-formed) 코드가 된다. void **와 같이 **를 이용한 것은 별다른 이유라기 보다는 최대한 사용자의 실수를 방지하기 위한 것이다.
template <typename T, typename = void>
struct Actor
{
void doSomething()
{
std::cout << "did some work." << std::endl;
}
};
template <typename T>
struct Actor<T, std::enable_if_t<std::is_integral<T>::value>>
{
void doSomething()
{
std::cout << "did some work for integral type." << std::endl;
}
};
template <typename T>
struct Actor<T, std::enable_if_t<std::is_floating_point<T>::value>>
{
void doSomething()
{
std::cout << "did some work for floating point type." << std::endl;
}
};
// ...
using std::string;
// ...
Actor<string> as;
as.doSomething(); // 'did some work.'
Actor<int> ai;
ai.doSomething(); // 'did some work for integral type.'
Actor<double> ad;
ad.doSomething(); // 'did some work for floating point type.'
Actor<int>를 예를 들어서 설명하면 내부적으로 다음과 같은 형태로 처리된다고 볼 수 있겠다.
두번째 specialization은 잘못된 코드임으로 무시된다. 나머지 primary template과 first specialization 사이에서 선택이 일어나야 하겠다. 이때 primary template의 두번째 template parameter에 지정된 default template argument보다 first specialization에서 명시적으로 두번째 template argument에 지정된 것이 우선시 되어 first specialization이 선택된다.
(Function template보다는 class template specialization에서 좀더 쓰임새가 있을것 같다...)
C++17 constexpr if statement
C++17에 도입되는 constexpr if statement 언어문법을 사용하게되면 앞서 std::enable_if를 사용하여 구현한 코드는 대부분 상당히 간소화될 것이다. 이 내용을 업데이트하는 현시점(2016/09/05)에 아직 이를 구현한 정식 릴리즈된 컴파일러는 없다는 것 같다. 개발중인 최신의 clang 컴파일러에서 사용가능하다고 한다.
우선 medianValue function template은 아래와 같이 간소화될 수 있을 것이다.
template <typename Container>
auto medianValue(Container const& c)
{
constexpr auto isRandomAccessIterator
= std::is_same<
std::random_access_iterator_tag,
typename std::iterator_traits<typename Container::iterator>::iterator_category
>::value;
if constexpr(isRandomAccessIterator) {
return c[c.size() / 2];
} else {
auto iter = c.begin();
std::advance(iter, c.size() / 2);
return *iter;
}
}
Actor class template의 specialization코드는 아래와 같이 if ~ else chain 모양새의 코드로 바뀌게 된다. 이런 형태의 if ~ else chain 코드의 길이가 길어지면 그닥 보기가 좋지 않다. switch에 대해서도 constexpr statement 문법이 추가된다면 이런식의 코드에 대해서 가독성이 좀더 좋아질 수는 있을 것 같다는 생각을 그냥 해보기만 한다. :)
template <typename T>
struct Actor
{
void doSomething()
{
if constexpr(std::is_integral<T>::value) {
std::cout << "did some work for integral type.\n";
} else if constexpr(std::is_floating_point<T>::value) {
std::cout << "did some work for floating point type.\n";
} else {
std::cout << "did some work.\n";
}
}
};