본문 바로가기
프로그래밍/C & C++

[C++] Function Templates

by 별준 2022. 12. 28.

References

  • Ch 1, C++ Templates The Complete Guide 2nd

Contents

  • Basic of function templates
  • Two-Phase Translation
  • Template argument deduction
  • Multiple template parameters
  • Default template arguments
  • Overloading function templates

Function Templates

함수 템플릿은 다른 타입들에 대해 호출될 수 있는 기능적인 동작을 제공합니다. 즉, 함수 템플릿은 함수족(a family of functions)라고 표현할 수 있습니다. 아마 잘 아시다시피 템플릿 함수는 함수의 몇몇 요소의 타입이 결정되지 않았다는 것만을 제외하면 일반 함수의 외형과 거의 동일합니다. 이 요소는 파라미터화되는데, 기본적인 내용이지만 간단한 예제를 통해서 살펴보겠습니다.

 

Defining the Template

아래 코드는 두 값 중 최댓값을 리턴하는 함수 템플릿입니다.

template<typename T>
T max(T a, T b)
{
    // if b < a then yield a else yield b
    return b < a ? a : b;
}

위의 함수 템플릿 정의는 함수 파라미터 전달되는 a와 b, 두 값 중 큰 값을 반환하는 함수족을 나타냅니다. 이 파라미터의 타입은 template parameter T이며, 아직 정해지지 않았습니다. 위의 코드에서 볼 수 있듯이 템플릿 파라미터는 아래와 같은 문법을 사용하여 명시됩니다.

template< comma-separated-list-of-parameters >

예제 코드에서 파라미터 목록은 'typename T' 이며, 꺽쇠 괄호로 둘러싸여 있습니다.  typename 키워드는 타입 파라미터(type parameter)를 도입합니다. typename이 C++ 프로그램의 템플릿 파라미터 중 가장 흔히 사용되는데, 다른 파라미터도 가능합니다. 이는 다른 포스팅에서 추후 다루도록 하겠습니다.

 

위 예제 코드에서 타입 파라미터는 T 입니다. 파라미터의 이름으로는 어떠한 식별자를 사용해도 괜찮으며, 주로 T를 사용합니다. 이 타입 파라미터는 호출자가 함수를 호출할 때 결정하는 임의의 타입을 나타냅니다. 기본 타입이든, 클래스이든 템플릿이 사용하는 동작을 제공하기만 한다면, 어떤 타입이라도 이 함수의 인자로 사용할 수 있습니다. 위 예제에서는 < 연산자를 사용하여 a와 b를 비교하고 있으므로, T 라는 타입은 < 연산자를 지원해야 합니다.

C++17 이전에는 max()의 정의에 따라 타입 T의 값을 반환해야 하므로 이 타입은 복사가 가능해야만 하지만, C++17 이후부터는 rvalue도 전달할 수 있다고 합니다.

 

전통적인 이유로 타입 파라미터를 정의할 때 typename이 아닌 class를 사용할 수도 있습니다. typename은 C++98 표준을 만드는 중에 도입되었는데, 그 전에는 class를 사용했었고 현재도 typename 대신 class를 사용할 수 있습니다. 따라서, 위에서 정의한 max() 템플릿은 아래와 같이 정의해도 됩니다.

template<class T>
T max(T a, T b)
{
    return b < a ? a : b;
}

의미상 두 함수는 동일하며, class를 사용하더라도 템플릿 인자로 < 연산자를 지원하는 타입이라면 어떤 타입이든 사용할 수 있습니다. 다만, T로 클래스 타입만을 사용할 수 있는게 아니더라도 클래스 타입만 가능하다고 오해할 수 있으므로 typename을 사용하는 것을 권장합니다.

 

Using the Template

위에서 정의한 max() 함수 템플릿은 아래와 같이 사용할 수 있습니다.

#include <iostream>
#include <string>

int main(int argc, char** argv)
{
    int i = 42;
    std::cout << "max(7,i):    " << ::max(7,i) << '\n';

    double f1 = 3.4;
    double f2 = -6.7;
    std::cout << "max(f1,f2):  " << ::max(f1, f2) << '\n';

    std::string s1 = "mathematics";
    std::string s2 = "math";
    std::cout << "max(s1,s2):  " << ::max(s1,s2) << '\n';

    return 0;
}

위 프로그램은 총 3번의 max() 템플릿 호출이 있으며, 프로그램의 출력은 아래와 같습니다.

위 코드에서 max() 템플릿을 호출할 때마다 ::으로 한정했다는 것에 주목해봅시다. 이렇게 한정하면 전역 네임스페이스에서 우리가 구현한 max() 템플릿을 찾습니다. 표준 라이브러리에도 std::max() 템플릿이 있기 때문에 ::으로 한정하지 않으면 어떤 개발 환경에서는 모호하다고 에러가 발생할 수 있습니다.

 

템플릿은 어떤 타입이라도 받을 수 있는 하나의 실체가 있는 함수로 컴파일되지 않습니다. 대신, 템플릿이 사용될 때마다 템플릿 코드로부터 각 타입에 맞는 실체가 있는 함수를 만듭니다. 따라서, 위의 예제 코드에서 max()는 세 가지 타입에 대해 각각 컴파일됩니다. 예를 들어, 첫 번째 max() 호출에서는 템플릿 파라미터 T로 int를 사용하며, 실제로 호출되는 함수는 다음과 같을 것 입니다.

int max(int a, int b)
{
    return b < a ? a : b;
}

이렇게 템플릿 파라미터를 실제 사용되는 타입으로 바꾸는 프로세스를 인스턴스화(instantiation)이라고 부릅니다. 그 결과, 템플릿의 인스턴스(instance of template)가 생성됩니다.

함수 템플릿을 사용하기만 했는데, 이러한 인스턴스화가 발생했음에 주목합니다 (인스턴스화하기 위해서 별도로 해야할 것이 없습니다).

 

void도 유효한 파라미터 인자이므로, 그 결과로 생성되는 코드도 유요합니다.

template<typename T>
T foo(T*)
{
}

void* vp = nullptr;
foo(vp);  // OK: deduces void foo(void*)

 

Two-Phase Translation

함수 템플릿 내에서 사용된 연산을 지원하지 않는 타입에 대해 템플릿을 인스턴스화하면 컴파일 에러가 발생합니다. 아래의 예제 코드를 살펴봅시다.

std::complex<float> c1, c2; // doesn't provide operator <
...
::max(c1, c2);              // ERROR at compile time

위 예제 코드를 컴파일해보면, 위와 같이 컴파일 에러가 발생합니다.

 

여기서 템플릿은 두 단계를 통해 컴파일됩니다.

  • definition time(정의 시간)에 인스턴스화 없이, 템플릿 파라미터는 무시하고 템플릿 코드 자체가 올바른지 체크합니다. 여기에는 세미콜론을 빼먹었다는 등의 문법 에러(syntax error), 템플릿 파라미터에 종속되지 않는 unknown name(type name, function name,...) 사용, 또는 템플릿 파라미터에 종속되지 않는 static assertion이 포함됩니다.
  • instantiation time에 모든 코드가 유효한 지 확인하기 위해서 템플릿 코드를 다시 체크합니다. 즉, 템플릿 코드에 종속되는 모든 부분은 두 번 검사합니다.

아래 예제 코드를 살펴봅시다.

template<typename T>
void foo(T t)
{
    undeclared();  // first-phase compile-time error if undeclared() unknown
    undeclared(t); // second-phase compile-time error if undeclared(T) unknown
    static_assert(sizeof(int) > 10, "int too small"); // always fails if sizeof(int) <= 10
    static_assert(sizeof(T) > 10, "T too small");     // fails if instantiated for T with size <= 10
}

이름을 두 번 검사한다고 하여 two-phase lookup이라고도 부르는데, 이와 관련한 내용은 다른 포스팅에서 다룰 기회가 있을 것 같습니다.

 

참고로 일부 컴파일러는 첫 번째 단계에서 full check를 수행하지 않기도 합니다. 따라서 템플릿이 한 번도 인스턴스화되지 않으면 일반적인 에러도 알아내지 못할 수 있습니다.

 

Compiling and Linking

템플릿은 이렇게 두 번 컴파일되기 때문에 실제로 템플릿을 다룰 때 중요한 문제가 발생할 수 있습니다. 한 가지가 바로 함수 템플릿을 사용하여 인스턴스화를 하려면 컴파일러가 템플릿의 정의를 (어느 시점에) 알아야 한다는 것 입니다. 따라서, 일반적인 함수는 컴파일과 링크가 분리되어 함수의 선언만 알고 있어도 그 함수를 사용하는 코드를 컴파일할 수 있지만, 템플릿을 사용하면 이 둘을 쉽게 분리할 수 없습니다 (선언과 정의). 이러한 문제를 해결하는 방법 또한 다른 포스팅에서 다루어 보도록 하겠습니다. 그 전까지는 모든 템플릿을 헤더 파일에 구현하는 간단한 방법을 사용할 예정입니다.

 


Template Argument Dedection

위에서 정의한 max()와 같은 템플릿 함수를 어떤 인자에 대해 호출하면, 전달받은 인자를 통해 템플릿 파라미터가 결정됩니다. 만약 파라미터 타입 T에 두 int를 전달하면, C++ 컴파일러는 T가 int라고 결론을 내립니다.

 

하지만, T는 타입의 일부(part)일 수도 있습니다. 예를 들어, max()가 상수 참조자(constant reference)를 사용하도록 선언했다고 가정해봅시다.

template<typename T>
T max(T const& a, T const& b)
{
    return b < a ? a : b;
}

여기에 인자로 int를 전달하면, 또 다시 T는 int로 추론되는데, 함수 파라미터가 int const&와 일치하기 때문입니다.

 

Type Conversion During Type Deduction

타입 추론에서는 automatic type conversion(자동 형변환)이 제한된다는 점에 유의해야 합니다.

  • call parameters를 참조자(reference)로 선언하면, 자명한 변환(trivial conversion)도 타입 추론에 적용되지 않습니다. 동일한 템플릿 파라미터 T로 선언된 두 인자의 타입은 완전히 일치해야 합니다.
  • call parameters를 값(value)로 선언하면, 오직 decay(데이터형 소실)되는 자명한 변환(trivial conversion that decay)만 지원됩니다. const 또는 volatile로 한정된 것은 무시되고, 참조자는 참조자 타입으로 변환되고, 배열(raw array)나 함수는 대응하는 포인터 타입으로 변환됩니다. 동일한 템플릿 파라미터 T로 선언된 두 인자에 대한 decayed 타입이 완전히 일치해야 합니다.

예시를 통해 위 내용을 살펴보도록 하겠습니다.

template<typename T>
T max(T a, T b);
...
int const c = 42;
max(i, c);        // OK: T is deduced as int
max(c, c);        // OK: T is deduced as int

int& ir = i;
max(i, ir);       // OK: T is deduced as int

int arr[4];
foo(&i, arr);     // OK: T is deduced as int*

위의 코드는 모두 정상적으로 타입이 추론되지만, 아래 코드에서는 에러가 발생합니다.

max(4, 7.2);     // ERROR: T can be deduced as int or double
std::string s;
foo("hello", s); // ERROR: T can be deduced as char const[6] or std::string

위와 같은 에러는 다음의 세 가지 방법으로 해결할 수 있습니다.

  1. 두 인자가 모두 일치하도록 인자를 변환 (max(static_cast<double>(4), 4.2))
  2. 컴파일러가 type deduction을 하지 않도록 T의 타입을 명시 (max<double>(4, 4.2))
  3. 파라미터가 다른 타입을 가질 수 있도록 템플릿을 수정

이번 포스팅에서는 3번 방법에 대해서 자세히 살펴봅니다.

 

Type Deduction for Default Arguments

기본 호출 인자에 대해서는 타입 추론을 하지 않는다는 점에 유의합니다.

template<typename T>
void f(T = "");
...

f(1); // OK: deduced T to be int, so that it calls f<int>(1)
f();  // ERROR: cannot deduce T

위와 같은 코드를 사용하고 싶다면, 템플릿 파라미터에 대한 기본 인자를 선언해야 합니다.

template<typename T = std::string>
void f(T = "");
...
f(); // OK

 


Multiple Templates Parameters

지금까지 본 것과 같이, 함수 템플릿은 두 종류의 파라미터를 가집니다.

  1. Template parameters (템플릿 파라미터). (함수 템플릿 이름 앞에 꺽쇠 내에 선언된 것)
  2. Call paramters (호출 파라미터). (함수 템플릿 이름 뒤 괄호 안에 선언된 것)

템플릿 파라미터는 원하는 만큼 얼마든지 선언할 수 있습니다. 예를 들어, 아래와 같이 두 호출 파라미터가 서로 다른 타입을 갖도록 max() 템플릿을 정의할 수 있습니다.

template<typename T1, typename T2>
T1 max(T1 a, T2 b)
{
    return b < a ? a : b;
}
...
auto m = ::max(4, 7.2); // OK, but type of first arugment defines return type

위의 템플릿에서 다른 타입의 호출 파라미터를 전달할 수 있다는 것은 좋지만, 몇 가지 단점이 있습니다. 그중 하나는 파라미터 타입 중 하나를 리턴 타입으로 사용한다면, 호출자의 의도와 관계없이 다른 파라미터에 대한 인자가 이 타입으로 변환될 수 있다는 것 입니다. 다시 말하면, 호출 인자의 순서에 따라 리턴 타입이 바뀌게 됩니다. 66.66과 42 중의 큰 값은 double 66.66 이지만, 42와 66.66 중의 큰 값은 int 66이 될 수 있다는 것 입니다.

C++에서 이 문제를 해결하는 방법은 3가지가 있습니다.

  • 리턴 타입을 명시하는 세 번째 템플릿 파라미터를 도입
  • 컴파일러가 리턴 타입을 알아내도록 함
  • 리턴 타입을 두 파라미터 타입의 "common type(공통 타입)"으로 선언

각 방법에 대해서 자세히 살펴보도록 하겠습니다.

 

Template Parameters for Return Types

Template argument deduction, 즉, 템플릿 인자 추론 덕분에 일반 함수를 호출하듯이 템플릿 함수를 호출할 수 있습니다. 따라서, 템플릿 파라미터에 대한 타입을 명시적으로 지정하지 않아도 됩니다. 하지만, 명시적으로 템플릿 파라미터의 타입을 지정할 수도 있습니다.

template<typename T>
T max(T a, T b);
...
::max<double>(4, 7.2); // instantiate T as double

 

템플릿과 호출 파라미터 간의 어떠한 연결점도 없고 템플릿 파라미터를 결정할 수 없는 상황에서는 호출할 때 템플릿 인자를 명확하게 지정해주어야 합니다. 예를 들어, 템플릿 함수의 리턴 타입을 정의하는 세 번째 템플릿 인자를 도입할 수 있습니다.

template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b);

하지만, 템플릿 인자 추론은 리턴 타입을 고려하지 않으며, 위 코드에서 RT는 함수 호출 파라미터의 타입에서도 사용되지 않습니다. 따라서, 위의 경우 RT의 타입은 추론될 수 없습니다. 따라서, 호출할 때 템플릿 인자 목록을 명시해주어야 합니다.

template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b);
...
::max<int, double, double>(4, 7.2); // OK, but tedious

 

위의 경우에는 템플릿 함수의 모든 템플릿 인자를 명시적으로 지정하고 있습니다. 하지만, 사실 첫 번째 인자만 명시적으로 알려주고 나머지는 추론되도록 하는 방식을 사용할 수 있습니다. 일반적으로 암묵적으로 추론될 수 없는 타입의 앞쪽에 위치하는 모든 인자 타입은 명시해야 합니다. 따라서, 바로 위의 버전에서 템플릿 파라미터의 순서만 변경해준다면 리턴 타입만 명시하여 호출할 수 있습니다.

template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b);
...
::max<double>(4, 7.2); // OK, return type is double, T1 and T2 are deduced

하지만, 지금까지 살펴본 max()의 수정된 버전들은 모두 큰 장점은 없습니다.

 

Deducing the Return Type

만약 리턴 타입이 템플릿 파라미터에 종속된다면 리턴 타입을 추론하는 간단하면서 가장 효과적인 방법은 컴파일러가 직접 이를 알아내는 것 입니다. C++14부터는 어떠한 리턴 타입도 선언하지 않는 방식(auto로 선언하긴 해야 함)으로 컴파일러가 이를 알아내도록 할 수 있습니다. (아래 코드는 C++14부터 사용 가능)

template<typename T, typename T2>
auto max(T1 a, T2 b)
{
    return b < a ? a : b;
}

실제로 함수 인자 목록에 이어서 '->'로 도입되는 리턴 타입없이 함수 리턴 자리에 auto를 사용하는 것은 함수 바디에서 리턴 구문을 통해 실제 리턴 타입을 추론하라는 것을 나타냅니다. 물론, 함수 바디 내에서 리턴 타입을 추론할 수 있어야만 합니다. 이를 위해서는 코드가 반드시 있어야 하며, 함수 바디 내에서 리턴 구문이 여러 개라면 모두 동일한 타입을 반환해야 합니다.

 

C++14 이전에는 컴파일러가 함수의 구현을 선언의 일부로 만들어서 리턴 타입을 결정하도록 하는 것이 가능했습니다. C++11에서 trailing return type 문법(->)을 활용하여 호출 파라미터를 사용할 수 있도록 했습니다. 즉, operator?:가 도출하는 타입으로부터 리턴 타입을 추론하도록 선언할 수 있습니다. (아래 코드는 C++11에서도 사용 가능)

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b)
{
    return b < a ? a : b;
}

여기서 리턴 타입은 operator?: 규칙에 따라 결정되는데, 상당히 정교하지만 직관적으로 예상한 결과가 나옵니다 (만약 a와 b가 서로 다른 수치 타입이라면, 결과로 공통되는 수치 타입을 알아냅니다).

 

아래 코드는 구현이 없는 선언이라는 점에 유의해봅시다.

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b < a ? a : b);

따라서, 위 코드에서 컴파일러는 컴파일 과정에 max()의 리턴 타입을 알아내기 위해서 파라미터 a와 b에 대해 operator?:를 적용합니다. 실제 구현이 이 부분과 일치할 필요는 없으며, operator?:의 조건으로 true를 사용해도 괜찮습니다.

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b);

하지만, 이 정의에는 심각한 문제가 있습니다. T가 참조자가 되면, 리턴 타입 또한 참조자가 될 수 있습니다. 따라서, T로부터 소실되는 타입(the type decayed from T)을 리턴해야 합니다. 아래 코드를 살펴봅시다.

#include <type_traits>

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true?a:b)>::type
{
    return b < a ? a : b;
}

여기서 타입 특질(type trait)인 std::decay<>를 사용했습니다. std::decay<>는 멤버로 가지고 있는 type을 사용하여 템플릿 파라미터로 전달받은 타입을 알아낼 수 있습니다. 이때, ::type은 전달받은 타입에 참조자가 있다면, 참조자를 해제하고 원래의 타입을 반환합니다. 참고로 std::decay<>의 멤버인 type은 타입이므로 액세스하기 위해서는 표현식을 typename으로 한정시켜주어야 합니다.

 

auto 타입의 초기화는 항상 decay(형 소실)된다는 점에 주목합시다. 리턴 타입이 그냥 auto일 경우에도 이 규칙이 리턴 값에 적용됩니다. 아래 코드에서 a 변수의 타입은 int가 됩니다.

int i = 42;
int const& ir = i; // ir refers to i
auto a = ir;       // a is declared as new object of type int

 

Return Type as Common Type

C++11부터는 표준 라이브러리에서 "the more general type"을 선택하는 방법을 제공합니다. std::common_type<>::type을 사용하면 템플릿 인자로 전달된 두 개(또는 그 이상)의 서로 다른 타입의 "common type(공통 타입)"을 도출할 수 있습니다.

아래 예제 코드르 살펴봅시다.

#include <type_traits>

template<typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b)
{
    return b < a ? : a : b;
}

std::common_type은 타입 특질이며, <type_traits>에 정의되어 있습니다. 이를 사용하면 결과로 얻은 타입을 type이라는 멤버를 통해 액세스할 수 있습니다. 이 특질을 사용하는 방법은 다음과 같습니다.

typename std::common_type<T1, T2>::type // since C++11

참고로 위에서는 std::common_type_t를 사용했는데, 이는 C++14부터 사용 가능한 문법이며 특질 이름 뒤에 _t를 덧붙이면 typename과 ::type을 생략할 수 있어서 더욱 간단하게 작성할 수 있습니다. 실제 표준 라이브러리의 구현을 보면 아래와 같이 using을 사용하고 있습니다.

std::common_type_t<T1, T2>  // since C++14

참고로 std::common_type<>의 구현은 여러 가지 복잡한 템플릿 프로그래밍을 사용합니다. 내부적으로는 operator?:의 언어 규칙이나 지정된 타입의 특수화(specialization)을 통해 타입을 결정합니다. 따라서, ::max(4, 7.2)나 ::max(7.2, 4)는 모두 double 타입의 7.2를 반환합니다. std::common_type<> 또한 decay 됩니다.

 


Default Template Arguments

템플릿 파라미터의 기본값을 정의할 수도 있습니다. 이러한 값을 default template arguments라고 부르는데, 어떤 종류의 템플릿에서도 사용할 수 있습니다. 앞에 위치한 템플릿 파라미터를 가리킬 수도 있습니다.

 

예를 들어, 여러 파라미터 타입을 가질 때, 템플릿 파라미터 RT를 두 인자의 공통 타입을 기본값으로 하도록 정의할 수 있습니다. 이를 정의하는 방법은 여러 가지가 될 수 있습니다.

먼저 operator?: 를 직접 사용할 수 있습니다. 하지만, 호출 파라미터 a와 b가 선언되기 전에 operator?:를 적용해야 하므로, 이들의 타입만 사용할 수 있습니다.

#include <type_traits>

template<typename T1, typename T2,
         typename RT = std::decay_t<decltype(true?T1():T2())>>
RT max(T1 a, T2 b)
{
    return b < a ? a : b;
}

(참조자가 반환되지 않도록 std::decay_t를 사용하고 있습니다)

위와 같이 구현하려면 전달된 타입의 default constructor를 호출할 수 있어야 합니다. std::declval을 사용하면 해결되긴 하지만 선언문이 조금 더 복잡해질 수 있습니다.

 

두 번째 방법으로는 리턴 타입의 기본값을 std::common_type<>을 사용하여 정의할 수 있습니다.

#include <type_traits>

template<typename T1, typename T2,
         typename RT = std::common_type_t<T1,T2>>
{
    return b < a ? a : b;
}

(std::commont_type<>은 데이터형 소실이 발생하므로 참조자가 반환되지 않습니다)

 

이렇게 구현하면 어떤 경우에서든지 호출자는 리턴 타입의 기본값을 사용할 수 있습니다.

auto a = ::max(4, 7.2);

또는, 다른 인자의 타입을 명시적으로 나열하고 이어서 리턴 타입을 명시할 수도 있습니다.

auto b = ::max<double, int, long double>(7.2, 4);

하지만 이렇게 하면, 리턴 타입만 명시할 수 없고 세 타입을 모두 나열해야 합니다.

리턴 타입을 첫 번째 파라미터로 명시하도록 변경하면,

#include <type_traits>

template<typename RT = long, typename T1, typename T2>
{
    return b < a ? a : b;
}

이제 아래와 같이 호출할 수 있습니다.

int i;
long l;
...
max(i, l);       // returns long
max<int>(4, 42); // returns int as explicitly requestesd

하지만 위 방식은 템플릿 파라미터의 자연적인 기본값이 있을 때만 가능합니다. 여기서는 리턴 타입이 이전 템플릿 파라미터에 종속되어 있습니다. 원칙적으로는 가능하지만, 이는 타입 특질에 기반한 기법을 사용해야 하므로 함수 정의가 복잡해지게 됩니다. 여기서 따로 다루지는 않습니다.

 

위와 같은 여러 가지 이유로 가장 쉽고 좋은 방법은 컴파일러가 리턴 타입을 추론하는 것 입니다.

 


Overloading Function Templates

일반 함수처럼 함수 템플릿도 오버로딩할 수 있습니다. 즉, 같은 이름의 함수이지만 다른 정의를 갖도록 하여 해당 이름의 함수가 호출될 때 C++ 컴파일러가 다양한 정의 중 하나를 선택하게 할 수 있습니다. 이를 결정하는 방법은 템플릿이 아니더라도 꽤 복잡한 편인데, 이번에는 템플릿에서의 오버로딩에 대해 알아보도록 하겠습니다.

 

아래 예제 코드는 함수 템플릿을 오버로딩하는 방식을 보여줍니다.

int max(int a, int b)
{
    return b < a ? a : b;
}

template<typename T>
T max(T a, T b)
{
    return b < a ? a : b;
}

int main(int argc, char** argv)
{
    ::max(7, 42);         // calls the non-template for two ints
    ::max(7.0, 42.0);     // calls max<double> (by argument deduction)
    ::max('a', 'b');      // calls max<char> (by argument deduction)
    ::max<>(7, 42);       // calls max<int> (by argument deduction)
    ::max<double>(7, 42); // calls max<double> (no argument deduction)
    ::max('a', 42.7);     // calls the non-template for two ints
}

위 예제 코드에서처럼 템플릿이 아닌 함수와 함수 템플릿이 같은 이름을 가질 수 있으며, 함수 템플릿이 템플릿이 아닌 함수와 같은 타입으로 인스턴스화될 수 있습니다. 모든 다른 요소들이 동일한 경우, 템플릿이 아닌 함수를 더 선호합니다. 위의 main 함수에서의 첫 번째 호출이 이 규칙이 적용된 경우 입니다.

::max(7, 42); // both int values match the nontemplate function perfectly

만약 더 일치하는 함수를 템플릿이 생성한다면, 템플릿이 선택됩니다. 두 번째와 세 번째 호출에서 이 규칙이 적용됩니다.

::max(7.0, 42.0); // calls the max<double> (by argument deduction)
::max('a', 'b');  // calls the max<char> (by argument deduction)

여기서 템플릿을 사용하면 double이나 char를 int로 변환하지 않아도 되므로 템플릿이 더 잘 맞다고 볼 수 있습니다.

 

또는 빈 템플릿 인자 목록을 명시적으로 지정하는 것도 가능합니다. 이 문법에 맞는 함수 호출은 템플릿뿐이며, 템플릿 파라미터는 호출 파라미터에서 추론됩니다.

::max<>(7, 42);

 

템플릿에서는 일반 함수와 달리 자동 형 변환(automatic type conversion)이 수행되지 않습니다. 따라서 마지막 호출은 템플릿이 아닌 일반 함수 버전이 호출됩니다 ('a'와 42.7 모두 int로 변환됨).

::max('a', 42.7); // only the non-template function allows nontrivial conversion

 

이번에는 리턴 타입만 명시적으로 지정할 수 있는 maximum 템플릿을 오버로딩하는 예제를 살펴보겠습니다.

template<typename T>
T max(T a, T b)
{
    return b < a ? a : b;
}

template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b)
{
    return b < a ? a : b;
}

위와 같이 정의되어 있는 경우, 다음과 같이 max()를 호출할 수 있습니다.

auto a = ::max(4, 7.2); // uses first template
auto b = ::max<long double>(7.2, 4); // uses second template

하지만, 아래의 경우에는 두 템플릿에 모두 일치하므로, 누구를 선호할 수 없어 'ambiguity error'가 발생합니다.

auto c = ::max<int>(4, 7.2); // ERROR: both function templates match

따라서, 함수 템플릿을 오버로딩한다면 어떠한 호출에서든지 단 하나만 일치하도록 주의해야 합니다.

 

아래 예제 코드는 포인터와 C 문자열에 대한 maximum 템플릿을 오버로딩하는 코드입니다.

#include <string>
#include <cstring>

// maximum of two values of any type
template<typename T>
T max(T a, T b)
{
    return b < a ? a : b;
}

// maximum of two pointers
template<typename T>
T* max(T* a, T* b)
{
    return *b < *a ? a : b;
}

// maximum of two C-strings
char const* max(char const* a, char const* b)
{
    return std::strcmp(b, a) < 0 ? a : b;
}

int main(int argc, char** argv)
{
    int a = 7;
    int b = 42;
    auto m1 = ::max(a, b); // max() for two values of type int

    std::string s1 = "hey";
    std::string s2 = "you";
    auto m2 = ::max(s1, s2); // max() for two values of type std::string

    int* p1 = &b;
    int* p2 = &a;
    auto m3 = ::max(p1, p2); // max() for two pointers

    char const* x = "hello";
    char const* y = "world";
    auto m4 = ::max(x, y); // max() for two C-strings
}

위의 코드에서 모든 오버로딩 구현이 인자를 값으로 전달했다는 점에 주목합시다. 일반적으로 함수 템플릿을 오버로딩할 때는 필요 이상으로 수정하지 않는 것이 좋습니다. 파라미터의 수를 바꾸거나 템플릿 파라미터를 명시하는 정도로만 수정하는 편이 좋습니다. 그렇지 않으면 예상치 못한 동작이 발생할 수 있습니다.

 

예를 들어, 인자를 참조자로 전달받는 max() 템플릿을 구현하고 값으로 두 개의 C 문자열을 받는 max()를 오버로딩한다면, 아래 코드에서와 같이 3개의 C 문자열에서의 최대값을 계산하는 버전의 max()를 사용할 수 없습니다.

#include <cstring>

// maximum of two values of any type (call by reference)
template<typename T>
T const& max(T const& a, T const& b)
{
    return b < a ? a : b;
}

// maximum of two C-strings (call by value)
char const* max(char const* a, char const* b)
{
    return std::strcmp(b, a) < 0 ? a : b;
}

// maximum of three values of any type (call by reference)
template<typename T>
T const& max(T const& a, T const& b, T const& c)
{
    return max(max(a, b), c); // error if max(a, b) uses call-by-value
}

int main(int argc, char** argv)
{
    auto m1 = ::max(7, 42, 68); // OK
    char const* s1 = "frederic";
    char const* s2 = "anica";
    char const* s3 = "lucas";
    auto m2 = ::max(s1, s2, s3); // run-time ERROR
}

위 코드에서는 세 개의 C 문자열을 비교하는 템플릿 내의 max() 호출에서 에러가 발생합니다.

return max(max(a, b), c);

max(a, b)가 새로운 임시 지역 변수를 만들었고 이 변수를 참조자로 반환해야 하는데, 임시 값은 리턴문이 종료되는대로 사라지기 때문에 main() 함수는 댕글링 참조자를 갖게 됩니다. 이러한 에러는 상당히 미묘한 부분이 있기 때문에 모든 경우에서 발생하는 것은 아닙니다.

 

main() 함수 내에서 max()를 처음 호출한 경우에는 동일한 문제가 발생하지 않습니다. 인자들의 값(7, 42, 68)을 위한 임시 변수가 생기기는 하지만, 이들은 main()에서 만들어진 값이기 때문에 함수 호출이 끝날 때까지 존재합니다.

 

위의 경우는 오버로딩을 해석하는 규칙에 따른 동작과 다르게 동작하는 하나의 예시일 뿐입니다. 이외에도 함수를 호출하기 전에 함수의 모든 오버로딩 버전을 선언해두도록 하는 것에 주의해야 합니다. 함수를 호출하는 시점에 모든 함수의 오버로딩 버전을 알고 있지 않다면 문제가 될 수 있습니다. 아래의 예제 코드가 바로 이 경우에 해당합니다.

#include <iostream>

// maximum of two values of any type
template<typename T>
T max(T a, T b)
{
    std::cout << "max<T>()\n";
    return b < a ? a : b;
}

// maximum of three values of any type
template<typename T>
T max(T a, T b, T c)
{
    return max(max(a, b), c); // use the template version even for ints
                              // because the following declaration comes too late
}

// maximum of two int values
int max(int a, int b)
{
    std::cout << "max(int,int)\n";
    return b < a ? a : b;
}

int main(int argc, char** argv)
{
    ::max(47, 11, 33); // uses max<T>() instead of max(int, int)
}

 

댓글