z 16. 특수화와 오버로딩 :: C++, 그래픽 기술 블로그

16.1 일반적인 코드가 별로 좋지 않을 때

일반적인 형식인 경우는 괜찮지만 특정 형식의 경우 효율이 급격하게 떨어지거나 예상과는 다르게 행동하는 경우가 존재합니다. 이런 경우 특정 형식에 대해서는 특수화를 통해 더 좋은 결과를 얻어 낼 수 있습니다. 만약 이를 다른 함수 두 개로 선택지를 주어진다면 사용자가 상황에 맞게 따로 사용해야하며, 이에 대한 인터페이스를 익혀두어야 하는 불편함이 존재하지만, 함수 템플릿의 오버로딩을 통해 다루면 이러한 상황을 좀 더 쉽게 다룰 수 있습니다.

16.2 함수 템플릿 오버로딩

같은 이름을 갖는 두 함수 템플릿은 공존할 수 있으며, 이들이 인스턴스화됐을 때 동일한 파라미터형을 가질 수도 있습니다.

template<typename T> 
int f(T)
{ 
    return 1;
}

template<typename T> 
int f(T*)
{ 
    return 2;
}

위의 경우 각각 T를 int*와 int로 치환했을 때, 함수는 동일한 파라미터형을 갖는 함수가 됩니다. 이러한 템플릿과 그의 인스턴스는 다 공존 가능합니다.

서명

두 함수가 서로 다른 서명을 가진다면 한 프로그램 내에서 공존할 수 있습니다. 함수의 서명은 다음 정보를 통합해 정의됩니다.

  1. 함수의 한정되지 않은 이름
  2. 해당 이름의 클래스 혹은 네임스페이스 영역과 이름이 내부 링크를 가진다면 이름이 선언된 번역 단위
  3. 함수의 const, volatile이나 const volatile 한정자
  4. 함수의 &나 && 한정
  5. 함수 파라미터의 형식
  6. 이 함수가 함수 템플릿에서 생성됐다면 반환형
  7. 이 함수가 함수 템플릿에서 생성됐다면 템플릿 파라미터와 템플릿 인자

 또한 이들을 인스턴스화할 때 오버로딩의 모호함을 일으키기 때문에 같은 영역에서 정의하면 항상 사용할 수 없습니다. 즉 함수는 공존 가능하지만, 오버로딩 해석에서 모호함이 생긴다면 이에 대해 오류를 일으키게 되고, 이는 다른 번역 단위에 존재할 때는 일어나지 않습니다.

오버로딩된 함수 템플릿의 부분 정렬

명시적인 템플릿 인자가 주어지지 않을때도 템플릿 인자 연역이 동작하여 함수가 선택되어야만 합니다. 이때 어떻게 인자 연역을 하는지 알아보겠습니다.

template<typename T> 
int f(T)
{ 
    return 1;
}

template<typename T> 
int f(T*)
{ 
    return 2;
}

int main() 
{
    std::cout << f(0);              // calls f<T>(T)
    std::cout << f(nullptr);        // calls f<T>(T)
    std::cout << f((int*)nullptr);  // calls f<T>(T*)
}

main문에서 1번째 코드는 2번째 f의 오버로딩이 포인터만을 받기에 쉽게 첫번째 f 함수에서 인스턴스화되는 것을 알 수 있습니다. 마지막 호출의 경우 두 템플릿에서 모두 인자 연역이 성공하고 인스턴스화 가능합니다. 이 호출의 경우는 모호하게 되지만, 부가적인 오버로딩 해석 기준이 사용되어, 좀 더 특수화된 템플릿에서 생성된 함수가 선택됩니다. 

공식 정렬 법칙

마지막 예제에서 두 번째 템플릿이 첫 번째 템플릿보다 더 특수한 이유는 첫 번째 템플릿이 어떤 인자형이라도 수용할 수 있는 반면, 두 번째 템플릿은 포인터형만을 허용하기 때문입니다. 하지만 이는 직관적이지 않으며 무엇이 더 특수화됐는지를 알아봐야만 합니다.

  • 기본 인자로 알 수 있는 함수 호출 파라미터와 사용되지 않은 줄임표 파라미터는 무시합니다.
  • 모든 템플릿 파라미터를 다음과 같이 치환하여 두 인자 형식 목록을 만들어 냅니다.
    1. 각 템플릿 파라미터를 고유한 고안된 형으로 바꿈
    2. 각 템플릿 템플릿 파라미터를 고유한 고안된 클래스 템플릿으로 바꿈
    3. 각 형식이 아닌 템플릿 파라미터를 적절한 형식의 고유한 고안된 값으로 바꿈
  • 두 번째 템플릿의 템플릿 인자 연역이 첫 번째 조합 목록에 대해 정확히 일치하지만 첫 번째 템플릿은 그렇지 않다면 첫 번쨰 템플릿이 두 번째보다 좀 더 특수화됐다고 간주합니다.

즉, 위의 마지막 예제에서 첫번째 f 함수는 int와 Int*를 둘다 인자로 받을 수 있지만, 2번째 f함수의 경우는 int만을 인자로 받아들일 수 있기에 더 특수화됐다는 의미입니다. 이렇게 우열 관계에 있는 경우는 한 쪽으로 정해지지만, 우열 관계가 존재하지 않는 경우는 호출이 모호해집니다.

템플릿과 템플릿이 아닌 것

함수 템플릿은 템플릿이 아닌 함수로 오버로딩될 수 있습니다. 그 외의 것이 모두 같다면 실체 함수 호출 시 템플릿이 아닌 함수를 선호합니다. 하지만 const와 참조 한정자가 다르다면 오버로딩 해석의 순서가 바뀔 수 있습니다. 

가변 함수 템플릿

가변 함수 템플릿은 부분 정렬하는 동안 몇 가지 특별 처리가 필요한데, 파라미터 꾸러미에 대한 연역에서는 한 파라미터가 여러 인자에 일치하기 때문입니다.

template<typename T>
int f(T*)
{
  return 1;
}

template<typename... Ts>
int f(Ts...)
{
  return 2;
}

template<typename... Ts>
int f(Ts*...)
{
  return 3;
}

int main()
{
  std::cout << f(0, 0.0);                          // calls f<>(Ts...)
  std::cout << f((int*)nullptr, (double*)nullptr); // calls f<>(Ts*...)
  std::cout << f((int*)nullptr);                   // calls f<>(T*)
}

이 경우에는 세 번째 함수 템플릿이 두 번쨰 보다 더 특수화됐다는 것은 앞서 말한 내용으로 알 수 있으며, 한 인자만 사용하는 경우를 보았을 때, 첫 번째와 세 번째 함수 템플릿이 대립하게 되는데, 이때는 첫 번째 템플릿이 단일 요소이지만 단일 요소 순열로 인자를 받는 것이 불가능하기 때문에 결국 첫번째 템플릿이 더 특수화되어 있기에 첫번째 템플릿이 선택됩니다. 즉, 함수 파라미터 꾸러미에서 나온 인자를 파라미터에 일치시키는 것은 불가능합니다.

16.3 명시적 특수화

함수 템플릿의 오버로딩과 부분 정렬을 통한 최적의 함수 템플릿 선택 방식을 함께 쓴다면 더욱 높은 효율성을 제공하기 위해 특수화된 템플릿을 일반적인 구현에 덧붙일 때에도 투명성을 보장할 수 있습니다. 하지만 클래스 템플릿은 오버로딩이 불가능하지만, 대신 명시적 특수화를 통해 특수화할 수 있습니다. 이는 전체 특수화라고도 불립니다.

 부분 특수화는 전체 특수화와 유사하지만 템플릿 파라미터 전체를 치환하는 것이 아니라 일부 파라미터는 남겨둬서 템플릿을 구현하는 방식입니다. 이 둘은 모두 소스코드에서 명시적입니다. 전체든 부분 특수화든 완전히 새로운 템플릿이나 템플릿 인스턴스를 도입하진 않지만 일반 템플릿에서 암묵적으로 선언된 인스턴스를 위해 대안으로서의 정의를 제공하며 이 것이 템플릿 오버로딩과 특수화의 가장 큰 차이점입니다.

전체 클래스 템플릿 특수화

전체 특수화는 template, <, >라는 세 토큰의 연속으로 만들어지며, 클래스 이름 선언자 뒤에 특수화를 선언할 템플릿 인자가 나옵니다.

template<typename T>
class S {};

template<>
class S<void> {};

전체 특수화를 구현할 때 일반 정의와 전혀 관계없이 클래스를 구성할 수 있습니다. 명시한 템플릿 인자의 목록은 템플릿 파라미터의 목록에 대응되어야만 합니다. 클래스 템플릿 특수화에서는 형식을 먼전 선언해 완전히 종속적인 형식을 생성하는 것이 유리할 때도 존재합니다. 전체 특수화 선언은 이러한 방식으로 일반 클래스 선언과 완전히 동일하지만 문법과 선언이 이전 템플릿 선언과 정확히 일치해야만 합니다.

 전체 특수화는 특정 일반 템플릿의 인스턴스를 치환하며, 한 프로그램에서 명시적으로 특수화된 템플릿과 생성된 템플릿이 공존할 수 없습니다. 이는 오류를 발생시킵니다.

전체 함수 템플릿 특수화

명시적 전체 함수 템플릿 특수화를 지원하는 문법과 원칙은 전체 클래스 템플릿 특수화와 비슷하지만, 오버로딩과 인자 연역이 더 추가되었습니다. 특수화할 템플릿을 인자 연역과 부분 정렬을 통해 결정할 수 있다면 전체 특수화를 선언할 때 명시적인 템플릿 인자를 생략할 수 있습니다.

template<typename T>
int f(T) { return 1 };    // #1

template<typename T>
int f(T*) { return 2 };   // #2

template<>
int f(int) { return 3 };  // #1의 특수화

template<>
int f(int*) { return 4 }; // #2의 특수화

전체 변수 템플릿 특수화

변수 템플릿도 전체 특수화할 수 있습니다.

template<typename T> constexpr std::size_t SZ = sizeof(T);
template<> constexpr std::size_t SZ<void> = 0;

전체 멤버 특수화

멤버 템플릿뿐 아니라 일반적인 정적 데이터 멤버와 멤버 함수도 전체를 특수화할 수 있습니다. 둘러싼 클래스 템플릿에 모두 template<>를 붙여 특수화됐음을 알려야 합니다.

16.4 부분 클래스 템플릿 특수화

전체 템플릿 특수화가 유용하긴 하지만 특정한 하나의 템플릿 인자 집합에 대해서만 클래스 템플릿을 특수화하는 것보다는 템플릿 인자 군에 대해 특수화하는 편이 더 자연스러울 때가 있습니다. 특히 포인터의 경우는 다 비슷하게 동작하기 때문에 void*에 대한 특수화를 만든뒤 이를 다른 포인터에서 사용하는 식으로 특수화로 만드는 것도 유효합니다. 이러한 부분 특수화 선언의 파라미터와 인자 목록에 대한 제약은 몇가지가 존재 합니다.

  1. 부분 특수화의 인자는 기본 템플릿의 대응되는 파라미터와 같은 종류여야 합니다.
  2. 부분 특수화의 파라미터 목록은 기본 인자를 가질 수 없습니다. 대신 기본 클래스 템플릿의 기본 인자를 사용할 수 있습니다.
  3. 부분 특수화의 형식이 아닌 인자는 비종속적인 값이거나 평범한 형식이 아닌 템플릿 파라미터여야 합니다.
  4. 부분 특수화의 템플릿 인자 목록은 항상 기본 템플릿의 파라미터 목록과 동일하지 않아야 합니다
  5. 템플릿 인자 중에 꾸러미 확장이 있다면 템플릿 인자 목록의 마지막에 나와야합니다.
template<typename T, int I=3>
class S;                        // 기본 템플릿

template<typename T>
classS<int, T>;                 // Error! 파라미터 종류 불일치

template<typename T = int>
class S<T, 10>;                 // Error! 기본 인자 없음

template<int I>
class S<int, I * 2>;            // Error! 형식이 아닌 표현식을 허용하지 않음

template<typename U, int K>
class S<U, K>;                  // Error! 기본 템플릿과 차이나지 않음

template<typename Ts...>
class Tuple;

template<Typename Tail, typename Ts...>
class Tuple<Ts..., Tail>;       // Error! 맨 끝에 있지 않은 꾸러미 확장

template<typename Tail, typename Ts...>
class Tuple<Tuple<Ts...>, Tail>;// OK! 꾸러미 확장이 중첩된 템플릿 인자 목록의 끝에 있음

템플릿이 사용될 때는 기본 템플릿은 항상 룩업되며, 템플릿 특수화를 선택할 때는 연결된 특수화들 중에서 인자를 비교해 결정하는데, 이는 인자 연역처럼 SIFNAE 원칙이 여기서도 적용됩니다. 일치하는 특수화가 여러개라면 가장 특수화된 것을 선택합니다. 그래도 여러개 존재한다면 모호함 오류가 납니다.

16.5 변수 템플릿 부분 특수화

문법 자체는 전체 변수 템플릿 특수화와 유사하지만, template<>은 실제 템플릿 선언으로 바뀌고, 변수 템플릿 이름 뒤에 나오는 템플릿 인자 목록은 템플릿 파라미터에 종속돼야만 합니다.

template<typename T> constexpr std::size_t SZ = sizeof(T);
template<typename T> constexpr std::size_t SZ<T&> = sizeof(void*);

변수 템플릿의 전체 특수화처럼 부분 특수화의 형식도 기본 템플릿과 일치할 필요는 없습니다.

template<typename T> typename T::iterator null_iterator;
template<typename T, std::size_t N> T* null_iterator<T[N]> = null_ptr;

변수 템플릿 부분 특수화에서 나열할 수 있는 템플릿 인자의 종류에 대한 법칙은 클래스 템플릿 특수화와 같으며, 실제 템플릿 인자 목록이 주어졌을 때 어떤 특수화를 선택할 것인지에 대한 법칙도 동일합니다.

'C++공부 > C++ Templates' 카테고리의 다른 글

19. 특질 구현  (0) 2022.07.05
18. 템플릿의 다형적 능력  (0) 2022.06.24
15. 템플릿 인자 영역  (0) 2022.06.22
14. 인스턴스화  (0) 2022.06.21
13. 템플릿에서 이름  (0) 2022.06.18

+ Recent posts