템플릿 부분 특수화 기법(partial template specialization)은 템플릿을 이용한 구조체나 클래스, 함수 등에서 일부 템플릿을 특정하여 오버로드 하는 기법을 말한다.
자세한 것은 MSDN을 참고 | https://msdn.microsoft.com/ko-kr/library/3967w96f.aspx
SFINAE(Substitution Failure Is Not An Error)은 템플릿의 치환이 틀리더라도, 다른 템플릿에서 알맞은 치환이 있을 수 있기 때문에 Substitution Failure (치환 실패 : 형식을 올바로 추론할 수 없을 경우) Is Not An Error(에러가 아니다) 라고 부른다.
자세한 것은 SFINAE를 검색해도 좋고, 개인적으로 해당 블로그를 참고했다.
SFINAE | https://github.com/jwvg0425/ModernCppStudy/wiki/SFINAE
이 두 가지 개념을 어떻게 쓸 수 있는지 이제부터 구체적인 예와 함께 설명하겠다.
std::invoke라는 함수를 처음 들어보는 사람이 있을 것이다. 함수와 인자를 받아 대신 실행해주는 함수인데, std::thread 에서 스레드 함수를 받을 때 사용되는 함수이다.
std::invoke 보다는 std::thread 함수를 써 본 사람이 많을 테니 std::thread 쪽으로 이야기를 해 보자. std::thread 는 인자로 받은 함수와 함수의 인자를 새로운 스레드에서 실행한다. 이 때 특이한 점은,
- 스레드 함수가 클래스의 멤버 함수일 경우 맨 처음 인자로 클래스의 주소를 넘겨주면 해당 클래스 인스턴스의 멤버 함수로 스레드가 만들어진다.
- 일반 전역 변수의 경우에는 그냥 함수와 인자만 넣어도 된다.
둘 모두 thread의 생성자를 쓰고, 인자로도 별다른 처리를 해 주지 않아도 된다. 심지어, 클래스 인스턴스의 포인터를 인자로 가지는 전역 함수조차 전역 함수라고 판단하고 스레드를 생성한다. 인자의 차이가 없기 때문에 함수 오버로딩은 아니다. 이제 이 함수의 비밀을 파헤쳐보자.
…
다 파헤쳤다. 까고 남은 건 invoke 함수에 인자로 받은 함수와 함수 인자를 넣어주는 것 말고는 없다. (그 외의 스레드 생성 등의 해설은 다른 곳을 찾길 바란다) 그렇다면, 결론은 이 invoke 함수를 사용해서 함수 오버로딩도 없이 클래스 멤버 함수와 전역 함수를 구분한다는 것이다. invoke가 무엇이길래?
invoke 함수를 파헤쳐보자. 이 함수는 <type_traits>라는 C++ 표준 라이브러리 헤더에 구현되어 있다. inline으로, 함수 본문까지 확인이 가능한 함수다.
함수의 원형은 다음과 같다.
template<class _Callable,
class... _Types> inline
auto invoke(_Callable&& _Obj, _Types&&... _Args)
{ // INVOKE a callable object
return (_Invoker<_Callable, _Types...>::_Call(
std::forward<_Callable>(_Obj), std::forward<_Types>(_Args)...));
}
(C++14는 반환 형식 추론이 가능해서 표준의 반환 형식 추론 부분을 제외했다)
중요한 것은, 반환으로 _Invoker라는 클래스의 _Call 이라는 함수를 호출한다는 것이다.
그러면 라는_Invoker 클래스는 무엇인가? 이것은 사실 구조체이다.
template<class _Callable>
struct _Invoker<_Callable>
: _Invoker_functor
{ // zero arguments
};
template<class _Callable,
class _Ty1,
class... _Types2>
struct _Invoker<_Callable, _Ty1, _Types2...>
: _Invoker1<_Callable, _Ty1>
{ // one or more arguments
};
뭔가 이상하다. 내용은 하나도 없고, template 형식과 상속 부분이 살짝 차이 난다. 그럼에도 둘 모두 _Invoker 라는 이름을 사용한다. 동일한 구조체인데!
이것이 가능한 이유는 두 구조체의 템플릿이 서로 모호하지 않기 때문이다. 이에 대한 이해는 글머리에 소개한 SFINAE 를 참고.
두 개의 _Invoker 구조체 중 첫 번째 것부터 보자. _Invoker_functor 는 이렇게 생겼다.
struct _Invoker_functor
{ // INVOKE a function object
template<class _Callable,
class... _Types>
static auto _Call(_Callable&& _Obj, _Types&&... _Args)
{ // INVOKE a function object
return (_STD forward<_Callable>(_Obj)(
_STD forward<_Types>(_Args)...));
}
};
생각보다 단순하다. 그냥 static 함수 하나만 있는 구조체다. 결론적으로는 다른 구조체들도 전부 끝에는 _Call을 들고 있을 것이다. 이제 알아볼 것은 어떻게 이렇게 둘로 나뉘냐는 것이다.
다음은 _Invoker1 구조체의 선언 부분이다.
template<class _Callable,
class _Ty1,
class _Decayed = typename decay<_Callable>::type,
bool _Is_pmf = is_member_function_pointer<_Decayed>::value,
bool _Is_pmd = is_member_object_pointer<_Decayed>::value>
struct _Invoker1;
template<class _Callable,
class _Ty1,
class _Decayed>
struct _Invoker1<_Callable, _Ty1, _Decayed, true, false>
: _If<is_base_of<
typename _Is_memfunptr<_Decayed>::_Class_type,
typename decay<_Ty1>::type>::value,
_Invoker_pmf_object,
_Invoker_pmf_pointer>::type
{ // pointer to member function
};
template<class _Callable,
class _Ty1,
class _Decayed>
struct _Invoker1<_Callable, _Ty1, _Decayed, false, true>
: _If<is_base_of<
typename _Is_member_object_pointer<_Decayed>::_Class_type,
typename decay<_Ty1>::type>::value,
_Invoker_pmd_object,
_Invoker_pmd_pointer>::type
{ // pointer to member data
};
template<class _Callable,
class _Ty1,
class _Decayed>
struct _Invoker1<_Callable, _Ty1, _Decayed, false, false>
: _Invoker_functor
{ // function object
};
맨 위부터 보자. 이건 단순히 선언만 돼 있다. 그런데 인자가 많다. 템플릿을 보니까 class 뿐만 아니라 bool 형식도 적혀 있다. 사실 이 부분도 템플릿을 자주 사용해보지 않은 사람들이라면 처음 본 광경일 수 있는데, 템플릿으로 값을 줄 경우 그 값을 상수처럼 쓸 수 있게 된다. 컴파일 타임이기 때문에 변수를 인자로 줄 수는 없다.
구조체의 템플릿 부분만 빼서 보자.
class _Callable,
class _Ty1,
class _Decayed = typename decay<_Callable>::type,
bool _Is_pmf = is_member_function_pointer<_Decayed>::value,
bool _Is_pmd = is_member_object_pointer<_Decayed>::value
1, 2번째 인자는 일반적으로 사용하는 템플릿이다. 호출 시점에서 값을 정해줘야 하는. 세 번째부터는 값이 정해져 있다. 특이한 점은, 값을 정하는데 이전에 선언된 템플릿을 사용한다. 왼쪽부터 오른쪽 순서로 값이 매겨지며 해당 인자보다 왼쪽에 있는 인자를 사용해서 템플릿을 특수화 할 수 있다. 이에 대한 자세한 내용은 글머리의 템플릿 부분 특수화를 참고.
여기서 다루는 것은 Invoke의 정확한 동작 원리는 아니기 때문에 세세한 형식 특질에 대한 설명은 생략하겠다. 실제로 어떤 동작을 하는지도 잘 모르겠고, 대체로 문장으로 쓴 형식 특질은 문장에 적힌 대로 동작하니까.
여하튼, 첫 번째 구조체에서 선언한 뒤 나머지 구조체는 첫 번째 구조체 템플릿의 부분 특수화를 했다. 모두 구조체의 이름이 같기 때문에 조건에 맞는 경우 해당 구조체가 상속하는 구조체의 _Call 함수가 호출된다.
이런 과정을 통해 invoke 함수가 동작한다. 별도의 함수명으로 사용자가 직접 관리하는 것보다 편리하다. 덧붙여서 SFINAE와 템플릿 부분 특수화에 대해서도 어떻게 쓰이는지 알아봤다.
이해가 되지 않는 것은 댓글로 물어보면 아는 한도 내에서 답변할 수 있도록 하겠다.
'프로그래밍 > C++' 카테고리의 다른 글
[C++17] filesystem (0) | 2017.05.22 |
---|---|
[C++11] 균일 초기화(중괄호 초기화) (0) | 2016.07.13 |
[C++11] 가변인자 탬플릿(Variadic Template) (0) | 2016.07.13 |
[C++11] enum class (0) | 2016.07.13 |