함수 템플릿
하나의 함수 정의를 여러 타입에 재사용하는 템플릿 문법과 타입 추론·명시적 인수·오버로딩 규칙을 익힙니다.
지금까지 객체 지향 프로그래밍의 핵심 개념을 다뤘습니다.
이번 장에서는 C++의 일반화 프로그래밍을 담당하는 템플릿(Template)을 다룹니다.
템플릿은 타입(Type)을 일반화(Generic)하여 코드를 재사용하고 유연성을 높이는 데 사용됩니다.
템플릿 프로그래밍을 통해 우리는 데이터 타입에 독립적인 함수나 클래스를 만들 수 있습니다.
이번 장에서는 템플릿의 첫 단계인 함수 템플릿(Function Templates)을 다룹니다.
먼저 함수 템플릿을 “정의 하나 → 호출 타입별 실제 함수 생성”이라는 흐름으로 잡아봅시다.
정의 하나가 자동으로 모든 타입을 지원하는 것이 아니라 호출마다 추론·후보 선택·요구 연산 검사가 일어난다.
- 호출
인자의 실제 타입을 읽는다.
- 추론
모든 위치에서 T가 일관되는지 확인한다.
- 후보 선택
변환 순위와 constraints를 비교한다.
- 본문 검사
선택된 타입에서 필요한 연산이 유효한지 확인한다.
- 생성
필요한 specialization을 생성한다.
추론 실패와 제약 실패는 다른 단계다. 변환을 숨기기보다 호출 계약을 명시한다.
왜 함수 템플릿을 사용하는가?
일반적으로 함수는 특정 데이터 타입에 대해 동작합니다.
예를 들어 두 정수를 더하는 함수와 두 실수를 더하는 함수는 다음과 같이 별도로 정의해야 합니다.
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을 위해
// 비교 연산과 값 복사를 지원하는 타입에 대해 동작하는 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 타입으로 변환
}
template <typename T>
T max(T a, T b) { return (a > b) ? a : b; }
// 함수 템플릿은 블록 밖에 선언합니다. 반환 타입 추론은 C++14부터 지원됩니다.
template <typename T1, typename T2>
auto flexible_max(T1 a, T2 b) { return (a > b) ? a : b; }
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
// 두 타입 매개변수는 각각 추론됩니다.
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
// (명시적 템플릿 인수 지정; 일반 함수는 후보가 아님)
return 0;
}컴파일러는 함수를 호출할 때 오버로딩된 함수와 템플릿 함수 중에서 가장 적합한 것을 선택합니다.
먼저 이름 탐색과 템플릿 인수 추론으로 후보를 구성하고, 인자 변환의 우열과 템플릿 부분 순서 등으로 최적 후보를 고릅니다. 다른 조건이 동등할 때 비템플릿 함수를 선호합니다. 명시적 템플릿 인수 지정은 별도의 “두 번째 우선순위”가 아니라 템플릿 후보를 지정하는 호출 형태입니다.
세 인자 예시의 두 인자 재귀 호출은 템플릿 정의 시 보이는 앞선 함수 템플릿을 사용하므로 내부 호출 안내도 출력합니다. 주석의 최댓값만 출력되는 예시는 아닙니다.
함수 템플릿의 장점과 한계
장점- 코드 재사용성: 동일한 로직을 다양한 타입에 대해 재사용할 수 있어 코드 중복을 줄입니다.
- 타입 안정성: 템플릿은 컴파일 시점에 타입을 결정하므로, 런타임 오류가 아닌 컴파일 타임 오류를 통해 타입 불일치를 잡아냅니다.
- 성능: 제네릭 포인터(
void*)나 기반 클래스 포인터를 사용하는 방식과 달리, 템플릿은 런타임 오버헤드 없이 최적화된 코드를 생성합니다.
- 컴파일 시간 증가: 템플릿은 사용되는 모든 타입에 대해 코드를 생성하므로, 컴파일 시간이 길어질 수 있습니다.
- 코드 크기 증가 (Code Bloat): 사용되는 타입의 조합만큼 코드가 생성되므로, 실행 파일의 크기가 커질 수 있습니다.
- 디버깅 어려움: 템플릿 오류 메시지는 종종 복잡하고 이해하기 어려울 수 있습니다.
- 모든 연산이 가능해야 함: 템플릿 함수 내부에서 사용되는 연산(예:
a > b,a + b)은T타입이 해당 연산을 지원해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.
함수 템플릿은 “중복을 줄이는 도구”이면서 동시에 “컴파일러가 타입별 함수를 생성하게 하는 약속”입니다.
사용 타입이 본문 연산을 지원하는지와 인스턴스 증가 비용을 함께 점검합니다.
호출 시 점검
마지막으로 호출문을 볼 때 확인할 세 가지 축을 정리합니다.
함수 템플릿은 T 추론, 명시적 인수 지정, 오버로딩의 우선순위를 함께 읽어야 합니다.
이 세 요소를 구분하면 추론 실패와 후보 선택 실패의 원인을 좁힐 수 있습니다.