함수 템플릿
독자 여러분, 지금까지 객체 지향 프로그래밍의 핵심 개념들을 깊이 있게 다루며 C++의 강력함을 경험했습니다.
이제 C++의 또 다른 강력한 기능인 템플릿(Template) 에 대해 학습할 차례입니다.
템플릿은 타입(Type)을 일반화(Generic)하여 코드를 재사용하고 유연성을 높이는 데 사용됩니다.
템플릿 프로그래밍을 통해 우리는 데이터 타입에 독립적인 함수나 클래스를 만들 수 있습니다.
이번 장에서는 템플릿의 첫걸음인 함수 템플릿(Function Templates) 에 대해 알아보겠습니다.
왜 함수 템플릿을 사용하는가?
일반적으로 함수는 특정 데이터 타입에 대해 동작합니다.
예를 들어 두 정수를 더하는 함수와 두 실수를 더하는 함수는 다음과 같이 별도로 정의해야 합니다.
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
// 만약 float, long, short 등의 타입에 대해서도 같은 작업을 하고 싶다면?
// float add(float a, float b) { return a + b; }
// ... 계속해서 함수를 오버로딩해야 함
위 코드에서 add
함수는 매개변수의 타입만 다르고, 함수 내부의 로직(return a + b;
)은 완전히 동일합니다.
만약 다른 타입에 대해서도 동일한 기능을 제공하고 싶다면,
매번 새로운 함수를 정의하고 오버로딩해야 합니다. 이는 코드의 중복을 유발하고 유지보수를 어렵게 만듭니다.
이러한 문제점을 해결하기 위해 등장한 것이 바로 함수 템플릿입니다.
함수 템플릿은 데이터 타입에 상관없이 동일한 작업을 수행하는 함수를 한 번만 정의하여 재사용할 수 있도록 합니다.
함수 템플릿의 정의와 사용
함수 템플릿은 template
키워드와 함께 타입 매개변수(Type Parameter)를 사용하여 정의합니다.
타입 매개변수는 실제 데이터 타입이 들어갈 자리임을 컴파일러에게 알려줍니다.
template <typename T> // 또는 template <class T>
반환타입 함수이름(T 매개변수1, T 매개변수2, ...) {
// T 타입에 대해 동작하는 코드
}
// 여러 개의 타입 매개변수 사용 가능
template <typename T1, typename T2>
반환타입 함수이름(T1 매개변수1, T2 매개변수2, ...) {
// T1, T2 타입에 대해 동작하는 코드
}
template <typename T>
또는template <class T>
:T
는 타입 매개변수입니다.typename
과class
는 이 맥락에서 동일하게 사용될 수 있습니다.typename
이 더 일반적인 용어로 권장됩니다.T
는 함수가 호출될 때 실제 데이터 타입(예:int
,double
,std::string
등)으로 대체됩니다.
#include <iostream>
#include <string> // std::string을 위해
// int, double 등 모든 타입에 대해 동작하는 max 함수 템플릿 정의
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
// 1. int 타입으로 max 함수 호출
// 컴파일러가 T를 int로 추론하여 int max(int, int) 함수를 생성 (템플릿 인스턴스화)
int i = max(10, 20);
std::cout << "Max of 10 and 20: " << i << std::endl; // 출력: 20
// 2. double 타입으로 max 함수 호출
// 컴파일러가 T를 double로 추론하여 double max(double, double) 함수를 생성
double d = max(3.14, 2.71);
std::cout << "Max of 3.14 and 2.71: " << d << std::endl; // 출력: 3.14
// 3. char 타입으로 max 함수 호출
char c = max('A', 'Z');
std::cout << "Max of 'A' and 'Z': " << c << std::endl; // 출력: Z
// 4. std::string 타입으로 max 함수 호출
std::string s = max(std::string("apple"), std::string("banana"));
std::cout << "Max of \"apple\" and \"banana\": " << s << std::endl; // 출력: banana
// 주의: 다른 타입끼리 호출 시 컴파일 오류 발생 (예: max(10, 3.14))
// 이유: T가 int와 double 중 어떤 타입이 되어야 할지 컴파일러가 결정할 수 없기 때문.
// template <typename T1, typename T2> T1 max(T1 a, T2 b) // 이렇게 타입을 분리하여 오버로딩 가능 (결과 타입 유의)
// 또는 명시적 타입 지정: max<double>(10, 3.14)
double mixed = max<double>(10, 3.14); // 명시적으로 T를 double로 지정
std::cout << "Max of 10 and 3.14 (explicit double): " << mixed << std::endl; // 출력: 10
return 0;
}
위 예시에서 우리는 max
함수를 한 번만 정의했지만, int
, double
, char
, std::string
등 다양한 타입에 대해 사용할 수 있습니다.
컴파일러는 함수 호출 시 전달된 인자의 타입을 기반으로 T
의 실제 타입을 추론하고, 해당 타입에 맞는 max
함수의 코드를 자동으로 생성합니다.
이 과정을 템플릿 인스턴스화(Template Instantiation) 라고 합니다.
명시적 템플릿 인수 지정
대부분의 경우 컴파일러는 함수 호출 시 전달된 인자의 타입을 통해 템플릿 인수를 자동으로 추론할 수 있습니다.
하지만 때로는 명시적으로 템플릿 인수를 지정해야 할 때가 있습니다.
컴파일러가 타입을 추론할 수 없을 때
- 함수 인자에 템플릿 타입이 포함되지 않는 경우.
- 여러 개의 타입이 혼합되어 컴파일러가 하나의
T
로 추론하기 어려울 때.
함수이름<타입1, 타입2, ...>(인자1, 인자2, ...);
#include <iostream>
template <typename T>
T add_default(T a, T b, T default_value) {
return a + b + default_value;
}
// 템플릿 매개변수 T를 함수의 반환 타입으로만 사용하고, 인자에는 사용하지 않는 경우
template <typename T>
T create_zero() {
return static_cast<T>(0); // 0을 T 타입으로 변환
}
int main() {
// add_default 호출 시 T는 int로 추론됨
std::cout << add_default(1, 2, 0) << std::endl; // 출력: 3
// 컴파일러가 T를 추론할 수 없음: create_zero()
// int zero_int = create_zero(); // 컴파일 오류! T를 추론할 수 없음.
int zero_int = create_zero<int>(); // 명시적으로 <int> 지정
std::cout << "Zero int: " << zero_int << std::endl; // 출력: 0
double zero_double = create_zero<double>(); // 명시적으로 <double> 지정
std::cout << "Zero double: " << zero_double << std::endl; // 출력: 0
// 서로 다른 타입의 인자가 전달될 때
// max 함수 템플릿 (예시 1의 코드와 동일)
// template <typename T> T max(T a, T b) { return (a > b) ? a : b; }
// double result = max(5, 3.14); // 컴파일 오류: T를 int로 할지 double로 할지 모호
double result1 = max<double>(5, 3.14); // 명시적으로 double로 지정
std::cout << "Max (5, 3.14): " << result1 << std::endl; // 출력: 5
// 또는, 함수 템플릿을 두 가지 타입 매개변수로 정의
template <typename T1, typename T2>
auto flexible_max(T1 a, T2 b) { // C++11 auto 반환 타입 추론
return (a > b) ? a : b;
}
double result2 = flexible_max(5, 3.14); // T1은 int, T2는 double로 추론. 반환 타입은 double로 추론
std::cout << "Max (5, 3.14) with flexible_max: " << result2 << std::endl; // 출력: 5
return 0;
}
함수 템플릿의 오버로딩
일반 함수처럼 함수 템플릿도 오버로딩될 수 있습니다.
즉, 동일한 이름의 템플릿 함수를 여러 개 정의할 수 있으며, 이들은 매개변수의 개수나 타입에 따라 구별됩니다.
#include <iostream>
#include <string>
// 1. 두 개의 T 타입 인자를 받는 max 함수 템플릿
template <typename T>
T max(T a, T b) {
std::cout << "두 T 타입 인자를 받는 max 호출\n";
return (a > b) ? a : b;
}
// 2. 세 개의 T 타입 인자를 받는 max 함수 템플릿 (오버로딩)
template <typename T>
T max(T a, T b, T c) {
std::cout << "세 T 타입 인자를 받는 max 호출\n";
return max(max(a, b), c); // 첫 번째 max 템플릿 호출
}
// 3. 특정 타입 (int)에 대한 특수화 (템플릿이 아님, 일반 함수)
// 템플릿보다 일반 함수가 우선순위가 높음
int max(int a, int b) {
std::cout << "int 타입에 대한 일반 max 함수 호출\n";
return (a > b) ? a : b;
}
int main() {
std::cout << max(10, 20) << std::endl; // 출력: int 타입에 대한 일반 max 함수 호출, 20
std::cout << max(3.14, 2.71) << std::endl; // 출력: 두 T 타입 인자를 받는 max 호출, 3.14
std::cout << max('A', 'Z') << std::endl; // 출력: 두 T 타입 인자를 받는 max 호출, Z
std::cout << max(10, 20, 30) << std::endl; // 출력: 세 T 타입 인자를 받는 max 호출, 30
std::cout << max(1.1, 2.2, 3.3) << std::endl; // 출력: 세 T 타입 인자를 받는 max 호출, 3.3
// int 타입의 인자로 3개 전달 시
std::cout << max<int>(10, 20, 30) << std::endl; // 출력: 세 T 타입 인자를 받는 max 호출, 30
// (명시적 템플릿 인스턴스화로 일반 int max 함수보다 템플릿이 선택됨)
return 0;
}
컴파일러는 함수를 호출할 때 오버로딩된 함수와 템플릿 함수 중에서 가장 적합한 것을 선택합니다.
일반적으로 다음과 같은 우선순위를 따릅니다.
- 정확히 일치하는 일반(비템플릿) 함수
- 명시적으로 지정된 템플릿 인수와 정확히 일치하는 템플릿 함수
- 타입 추론을 통해 가장 적합하게 인스턴스화될 수 있는 템플릿 함수
함수 템플릿의 장점과 한계
장점
- 코드 재사용성: 동일한 로직을 다양한 타입에 대해 재사용할 수 있어 코드 중복을 줄입니다.
- 타입 안정성: 템플릿은 컴파일 시점에 타입을 결정하므로, 런타임 오류가 아닌 컴파일 타임 오류를 통해 타입 불일치를 잡아냅니다.
- 성능: 제네릭 포인터(
void*
)나 기반 클래스 포인터를 사용하는 방식과 달리, 템플릿은 런타임 오버헤드 없이 최적화된 코드를 생성합니다.
한계
- 컴파일 시간 증가: 템플릿은 사용되는 모든 타입에 대해 코드를 생성하므로, 컴파일 시간이 길어질 수 있습니다.
- 코드 크기 증가 (Code Bloat): 사용되는 타입의 조합만큼 코드가 생성되므로, 실행 파일의 크기가 커질 수 있습니다.
- 디버깅 어려움: 템플릿 오류 메시지는 종종 복잡하고 이해하기 어려울 수 있습니다.
- 모든 연산이 가능해야 함: 템플릿 함수 내부에서 사용되는 연산(예:
a > b
,a + b
)은T
타입이 해당 연산을 지원해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.