이 챕터는 함수 템플릿 오버로드와 특수화에 대해 이야기하고 있다.
제목에서 함수 템플릿을 특수화하지 말라고 하는데, 이는 함수 템플릿 특수화의 낮은 직관성에 기인한다.
일반 함수 - 함수 템플릿 - 함수 템플릿 오버로드 - 함수 템플릿 특수화의 차이와
인스턴스화가 발생하는 우선 순위에 대해 짚고 넘어갈 수 있는 챕터이다.
신참 질문
C++에 존재하는 템플릿의 두 종류는 무엇이며, 각각 어떻게 특수화되는가?
C++ 템플릿은 다음 두 가지 종류가 있다.
- 함수 템플릿
- 클래스 템플릿
보통의 C++ 클래스와 마찬가지로 클래스 템플릿 역시 오버로드를 할 수 없다.
역시 C++ 함수와 마찬가지로 함수 템플릿 역시 오버로드가 가능하다.
위 이야기는 C++ 규칙에 따른 당연한 결과이므로 크게 새로운 부분이 없지만,
특수화의 경우 클래스 템플릿과 함수 템플릿의 가능 여부를 알아두는 것은 꽤나 중요한 일이다.
다음 예제를 살펴보자.
// 클래스 템플릿
template<typename t> // A
class X {};
// 오버로드된 두 개의 함수 템플릿
template<typename t> // B
void function(T t) {}
template<typename t> // C
void function(int i, T t) {}
위 템플릿들처럼 특수화되지 않은 템플릿들은 기본 템플릿(primary template)이라고 한다.
기본 템플릿들은 특수화될 수 있다.
위에서 얘기했던 클래스 템플릿과 함수 템플릿의 특수화 가능 여부 차이는 아래와 같다.
클래스 템플릿은 완전/부분 특수화가 모두 가능하나,
함수 템플릿은 완전 특수화만 가능하지, 부분 특수화는 불가능하다.
그러나, 함수 템플릿은 오버로드를 통해 부분 특수화와 같은 효과를 얻은 수 있다.
마지막으로, 함수 템플릿들의 여러 오버로드 버전들을 살펴보고 각각 어떤 상황에서 호출되는지 알아보자.
오버로드 해소 규칙은 적어도 고수준에서는 상당히 단순하며, 전형적인 2 계급 시스템으로 표현할 수 있다.
다음 코드들을 살펴보면서 규칙들을 자세히 확인해 보자.
기본 템플릿들은 특수화될 수 있다.
위에서 얘기했던 클래스 템플릿과 함수 템플릿의 특수화 가능 여부 차이는 아래와 같다.
클래스 템플릿은 완전/부분 특수화가 모두 가능하나,
함수 템플릿은 완전 특수화만 가능하지, 부분 특수화는 불가능하다.
그러나, 함수 템플릿은 오버로드를 통해 부분 특수화와 같은 효과를 얻은 수 있다.
마지막으로, 함수 템플릿들의 여러 오버로드 버전들을 살펴보고 각각 어떤 상황에서 호출되는지 알아보자.
오버로드 해소 규칙은 적어도 고수준에서는 상당히 단순하며, 전형적인 2 계급 시스템으로 표현할 수 있다.
다음 코드들을 살펴보면서 규칙들을 자세히 확인해 보자.
// 클래스 템플릿
template<typename T> // A
class X {};
template<typename T> // a-1. A의 포인터 형식에 대한 부분 특수화
class X<T*> {};
template<> // a-2. A의 int 타입에 대한 완전 특수화
class X<int> {};
// 오버로드된 두 개의 함수 템플릿
template<typename T> // B
void function(T t) {}
template<typename T> // C
void function(int i, T t, double d) {}
template<typename T> // D. B와 C의 오버로드 함수. 이것은 부분 특수화가 아니다.
void function(T *t) {}
template<> // b-1. B의 int 타입에 대한 완전 특수화
void function<int>(int i) {}
void f(double d) {} // B, C, D를 오버로드하는 일반 함수 (b-1은 오버로드하지 않는다)
1. 비템플릿 함수들은 1급 시민이다.
호출문과 일치하는 매개변수 형식들을 가진 보통의 함수와 템플릿 함수가 공존하는 경우 항상 보통 함수가 우선 선택된다.
2. 호출과 최소한으로도 일치하는 1급 시민(일반 함수)가 없을 경우, 2급 시민이라 할 수 있는 기본 함수 템플릿들을 고려한다.
그런 기본 함수 템플릿들이 여러 개 있는 경우(즉, 오버로드된 버전이 여럿 존재할 경우)
매개변수 형식들이 가장 잘 일치하며 "가장 구체적인" 것이 선택되는데, 이때 다음의 규칙들이 적용된다.
2-1. "가장 구체적인" 기본 함수 템플릿이 하나만 있다면 그것이 선택된다.
만일 그 기본 템플릿이 주어진 매개변수 형식들에 특수화된 것이라면, 그 특수화 버전이 선택되며,
그렇지 않으면 주어진 매개변수들에 맞는 템플릿 인스턴스가 만들어진다.
2-2. "가장 구체적인" 기본 함수 템플릿들이 여러 개인 경우 어느 것이 더 적한한지 컴파일러가 판단할 수 없으므로,
호출은 '애매한' 것이 된다.
프로그래머는 어떠한 것을 호출하고자 하는지 좀 더 명시적으로 지정해야 한다.
즉, 정리하면, 다음 순서라는 것이다.
일반 함수 >>> 기본 함수 템플릿 >>> 오버로드 해소 >>> 특수화 함수 템플릿
결단코 특수화된 함수 템플릿은 기본 함수 템플릿들과 나란히 우선 순위를 비교당할 수 없다.
기본 함수 템플릿 선에서 먼저 어떠한 녀석이 선택되어진 이후에나,
그 기본 함수 템플릿을 특수화한 녀석들 중에서 가장 가까운 녀석을 찾는 것이다.
위와 같이 오버로드와 특수화의 우선 순위가 정해진 데에는 핵심적인 이유가 있다.
"특수화는 오버로드 되지 않는다"
"오버로드는 일반 함수 또는 기본 함수 템플릿들에 대해서만 적용된다"
"따라서, 오버로드 해소 단계에서는 특수화 함수 템플릿은 전혀 고려되지 않는다"
이러한 우선 순위 규칙들을 이용하면, 다음 사용례와 같이 적용됨을 확인할 수 있다.
bool b;
int i;
double d;
f(b); // B가 호출됨. B로 오버로드 해소가 되었고, T는 bool이 된다.
f(i, 42, d); // C가 호출됨. C로 오버로드 해소가 되었고, T는 int가 된다.
f(&i); // D가 호출됨. D로 오버로드 해소가 되었고, T는 int가 된다.
f(i); // b-1이 호출됨. B로 오버로드 해소가 되었고, 그 중 특수화된 b-1이 선택됨.
f(d); // double 타입의 일반 함수가 존재하므로, 당연히 1순위.
다시 한번 얘기하지만, 특수화된 함수 템플릿은 기본 함수 템플릿이 결정된 이후에나 우선 순위를 따져볼 수 있다.
고수 질문
다음 코드의 마지막 줄에서 호출되는 것은 어떤 f 일까? 그리고 그것이 왜 호출될까?
template<typename T> // A
void f(T) {}
template<typename T> // B
void f(T*) {}
template<> // C. B의 특수화 버전
void f<int>(int*) {}
//...
int* p;
f(p); // 어떤 f가 호출될까?
A와 B, 두 개의 오버로드 함수 템플릿중 포인터 타입을 사용한 B가 더 가까워 오버로드 해소가 되었고,
그 B의 특수화 버전인 C가 타입이 더 근접하므로 C가 호출된다.
지금 우리는 규칙을 알고 있기에 쉽게 정답을 얘기할 수 있었지만
특수화된 함수 템플릿은 기본 함수 템플릿이 정해진 뒤에 비로소 우선 순위를 결정할 수 있다는 전제를 모르고,
아래 예제를 봤다면 아마도 너무나 당연하게 오답을 얘기했을 것이다.
template<typename T> // A
void f(T) {}
template<typename T> // B
void f(T*) {}
template<> // C. A의 특수화 버전
void f<int*>(int*) {}
//...
int* p;
f(p); // 어떤 f가 호출될까?
매개변수가 int* 일 때를 위한 특수화를 구체적으로 작성했으며, 예제의 호출문에 나온 매개변수도
정확히 int* 이므로 당연히 int* 를 위한 특수화가 호출되어야 하지 않을까??? 라는 착각에 빠지기 쉽다는 것이다.
하지만, 이젠 알 수 있듯이 정답은 B가 호출됨이다.
이런 경우 애초에 매개변수 형식과 정확히 일치하는 함수를 호출하고 싶었으면,
가장 단순하고 좋은 방법은 일반 함수를 작성하는 것이다.
지금까지 살펴본 바와 같이 비슷비슷한 형태의 기본 함수 템플릿들이 오버로드 되어 있고,
그 오버로드 된 녀석들의 특수화 버전까지 존재하는 경우 그 코드의 직관성은 상당히 떨어지게 되어 있다.
이 같이 비직관적인 코드를 피하기 위해서는
기본 함수 템플릿 오버로드와 특수화는 병행하지 않는 것이 바람직하다.
대안 제시
책에서는 다음과 같이 위에서 보여졌던 비직관적인 코드들의 문제에 대한 대안으로 다음과 같이
함수 템플릿과 클래스 템플릿과 정적 함수를 일종의 template-method 패턴같은 형태로 이용한 방법을 보여준다.
// 전방 선언
template <typename T>
struct FImpl;
template <typename T>
void function(T t)
{
FImpl<T>::f(t); // 이 코드라인은 사용자가 수정하지 않는다.
}
template <typename T> // 필요하다면 이 녀석을 특수화한다
struct FImpl
{
static void f(T t);
}
특수화가 필요할만한 기본 함수 템플릿을 작성하는 경우,
그것을 특수화와 오버로드가 되지 않는 기본 함수 템플릿으로 작성한 이후,
그 함수 템플릿이 그와 동일한 시그너쳐를 가지는 정적 함수를 포함한 클래스를 이용할 것.
사용자는 그 클래스를 얼마든지 자유롭게(완전/부분이든) 특수화할 수 있으며,
그 특수화가 함수 템플릿의 오버로드 해소의 결과에도 영향을 미치지 않으므로 목적한 바를 직관적으로 해결할 수 있다.
'책 스터디' 카테고리의 다른 글
[Exceptional C++ Style] 18. 클래스 가상성 (0) | 2023.04.10 |
---|---|
[Exceptional C++ Style] 16. private는 얼마나 비공개적인가? (1) | 2023.04.10 |
[Exceptional C++ Style] 14. 클래스 객체 생성(소멸) 순서의 중요성 (0) | 2023.04.10 |
[Exceptional C++ Style] 8. 템플릿 친구 만들기 (friend function template) (0) | 2023.04.10 |
[Exceptional C++ Style] 1. vector의 올바른/잘못된 용법 (0) | 2023.04.10 |