#include <iostream>
void foo(unsigned int i)
{ std::cout << "unsigned " << i << "\n"; }
template <typename T>
void foo(const T& t)
{
std::cout << "template " << t << "\n";
}
int main()
{
foo(42);
}
>>
template 42
즉 컴파일이 메뉴얼대로 알아서 간단한걸 찾는다?
#include <iostream>
int negate(int i) { return -i; }
template <typename T>
typename T::value_type negate(const T& t) {
return -T(t);
}
int main()
{
std::cout << negate(42) << std::endl;
}
>>
-42
T::value_type negate(const T& t)
로 가서
- 아니!!!!!
첫번째 후보군으로 들어가는게아니라 바로 int negate(int i) 함수로 들어간다
왜냐면 위의 위의 예제에서 42 를 unsinged int 로 치환 해야해서 한단계 더 거치기 때문에 다른 후보군 부터 보는데
이번 예제는 그렇지 않기 때문에 바로 int negate(int i) 로 들어간다.
int ::value_type negate(const int& t) {
/* ... */
}
컴파일러는 이런 코드를 만든다
하지만 int 에는 value_type 이라는 멤버가 없으므로 에러이다
이러한 에러를 치환에러라 정의하고
템플릿 인자 치환에 실패할 경우 (위 같은 경우)
컴파일러는 이 오류를 무시하고,
"그냥 오버로딩 후보에서 제외하면 된다"
그리고 두번째 후보로 ( 아니!! 첫번쨰로 얘 들어간다 위의 내용은 여기로 들어가면 이렇게 되었을것이다 라는 설명을 위해 한것? .)
int negate(int i) { return -i; }
#include <iostream>
int negate(unsigned int i) { return -i; }
// #1
// template <typename T>
// typename T::value_type negate(const T& t) {
// return -T(t);
// }
// #2
template <typename T>
void negate(const T& t) {
typename T::value_type n = -t();
}
int main()
{
std::cout << negate(42) << std::endl;
}
>>
컴파일 에러
int negate(unsigned int i)
unsigned int 로 한단 계를 더 거치게 해서 template 후보군 부터 검사하게 했다. template <typename T> typename T::value_type negate(const T& t) // 함수의 선언부 { return -T(t); // 정의부 }
- 선언부에
T::value_type
이것 때문에 치환 오류가 발생시 -> SFINAE 로 처리 되어서 컴파일 에러 발생 하지 않는다.- 근데 #2 처럼 정의부에
typename T::value_type n = -t();
로 치환 에러가 나면 그냥 컴파일 에러로 처리한다 ( SFINAE 범위밖 )
enable_if
이다.#include <iostream>
#include <cstddef>
template <bool, typename T = void>
struct enable_if {};
template <typename T>
struct enable_if<true, T>
{
typedef T type;
};
template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
void do_stuff(T& t)
{
std::cout << "do_stuff integral\n";
}
template <class T, typename std::enable_if<std::is_class<T>::value, T>::type* = nullptr>
void do_stuff(T& t)
[
// 일반적인 클래스 들을 받음
]
do_stuff(int 변수)
와 같이 함수를 호출하면 std::is_integral<int\>
가 참 이기 때문이다.std::is_class<int\>
가 false 이므로 내부에서 type이 정의 되지 않은 일반적인 형태의struct enable_if
가 선택 되서 치환 오류가 발생하기 때문이다.// Create the vector {8, 8, 8, 8}
std::vector<int> v1(4, 8);
// Create another vector {8, 8, 8, 8}
std::vector<int> v2(std::begin(v1), std::end(v1));
// Create the vector {1, 2, 3, 4}
int arr[] = {1, 2, 3, 4, 5, 6, 7};
std::vector<int> v3(arr, arr + 4);
template <typename T>
class vector {
vector(size_type n, const T val);
template <class InputIterator>
vector(InputIterator first, InputIterator last);
...
}
위 두 생성자 모두 두 개의 인자를 받는데,
두 번째의 경우 인자로 같은 타입인 두 객체가 전달된다면 오버로딩 됩니다. - 해당 생성자는 반복자를 받기 위해 만들어진 것이기에, 템플릿 인자 이름이 InputIterator 으로 되어 있지만 실제로는 어떠한 의미도 가지지 않습니다. - (그냥 그 자리에 T 가 와도 동작하는 방식은 똑같을 것입니다.)
문제는 v1(4, 8) 과 같이 생성자를 호출하였을 경우, 프로그래머는 첫 번째 생성자를 의도한 것이기겠지만,
실제로는 두 번째 생성자가 호출됩니다.
왜냐하면 size_type 이 대개 unsigned 로 정의되어 있지만, 4 의 경우 그냥 signed 이므로 더 잘 맞는 후보군은 두 번째 것이 되기 때문이죠.
따라서 라이브러리 제작자들은 이와 같은 상황을 피하기 위해 enable_if 를 이용해서 InputIterator 가 정말로 반복자일 때만 오버로딩 될 수 있도록 제한하였습니다.
template <class _InputIterator>
vector(
_InputIterator __first,
typename enable_if<__is_input_iterator<_InputIterator>::value &&
!__is_forward_iterator<_InputIterator>::value &&
/* ... more conditions... */ _InputIterator>::type __last);
실제 생성자 소스코드.
위 생성자의 경우 InputIterator 가 입력 반복자(input iterator) 이고 정방향 반복자(forward iterator)가 아닐 때 오버로딩 됩니다.
반복자가 정방향 반복자일 경우 다른 오버로딩이 존재하는데, 왜냐하면 반복자가 정방향 반복자일 경우 vector 생성을 좀 더 효율적으로 할 수 있기 때문이죠.
앞에서도 말했듯이 enable_if 는 C++ 11 표준 라이브러리 여기저기에서 사용되고 있습니다. string 의 append 함수 역시 위 예시와 매우 비슷한 용도로 사용되고 있습니다.
template <class T>
typename std::enable_if<std::is_arithmetic<T>, bool>::type signbit(T x) {
// implementation
}
솔직히 말해서 std::enable_if 를 사용하는 코드를 보면 그리 깔끔하지 않습니다.
만일 여러분이 enable_if 를 사용하게 된다면, 함수의 선언 부분과 리턴 타입을 쉽게 알아보기 힘들 것입니다.
하지만 std::enable_if 는 코드 상에서 함수의 정의 부분 외에는 거의 사용되지 않습니다.
마지막으로, C++ 표준 라이브러리에서는 비록 enable_if 가 코드를 좀 더 난잡하게 만들더라도,
좀 더 복잡하지만 깔끔하게 나타내는 방식 대신에 알아보기 쉬운 enable_if 를 적극 사용하고 있습니다.