함수 템플릿
하나의 함수 정의를 여러 타입에 재사용하는 템플릿 문법과 타입 추론·명시적 인수·오버로딩 규칙을 익힙니다.
지금까지 객체 지향 프로그래밍의 핵심 개념을 다뤘습니다.
이번 장에서는 C++의 일반화 프로그래밍을 담당하는 템플릿(Template)을 다룹니다.
템플릿은 타입(Type)을 일반화(Generic)하여 코드를 재사용하고 유연성을 높이는 데 사용됩니다.
템플릿 프로그래밍을 통해 우리는 데이터 타입에 독립적인 함수나 클래스를 만들 수 있습니다.
이번 장에서는 템플릿의 첫 단계인 함수 템플릿(Function Templates)을 다룹니다.
먼저 함수 템플릿을 “정의 하나 → 호출 타입별 실제 함수 생성”이라는 흐름으로 잡아봅시다. 앞 장의 함수 호출·값 전달·오버로딩을 바탕으로 읽습니다. 여기서 타입에 독립적이라는 말은 타입 이름을 매개변수로 바꾼다는 뜻입니다. 선택한 타입이 본문의 비교·복사 같은 연산까지 지원해야 실제로 사용할 수 있습니다.
함수 템플릿 호출은 추론부터 인스턴스 생성까지 이어진다
본문 첫 예제의 max(10,20)에서 T=int의 관계·정의의 int 비교/값 복사 적합성·실행 a 10/b 20·반환 20을 추적할 수 있다. 컴파일러 내부 pass 순서가 아닌 호출 준비 장부와 실행 경계를 표시하며 10/3.14 추론 실패와 비교 미지원 타입 오류는 별도 입력으로 둔다.
C++ · FUNCTION TEMPLATE CALL
max(10, 20)는 추론과 후보 선택을 거쳐 호출된다
본문 첫 예제의 template<typename T> T max(T a, T b)를 기준으로 읽습니다. 템플릿 이름 하나가 모든 타입의 연산을 보장하지는 않습니다.
- 호출 인자 확인
max(10, 20)에는 두 int 인자가 전달됩니다. - 템플릿 인수 추론
두 위치에서
T = int로 일치합니다.max(10, 3.14)는 int와 double 추론이 충돌합니다. - 사용 가능한 후보 선택
이름 탐색, 추론 결과, 인자 변환, 제약이 있다면 그 제약을 확인하여 가장 적합한 후보를 고릅니다.
- 선택된 함수 본문 검사
선택된 타입이
a > b와 값 복사 등 본문에서 사용한 연산을 지원해야 합니다. - 필요한 특수화 인스턴스화
해당 템플릿이 선택되면
max<int>의 정의가 필요에 따라 인스턴스화되고 결과는 20입니다.
오버로드가 추가되면: 본문 후반의 int max(int, int)처럼 다른 조건이 동등한 비템플릿 후보가 선택될 수 있습니다. max<int>(10, 20)은 템플릿 인수를 명시합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 8개 노드와 7개 간선이다. 첫 예제의 max(10,20)은 호출 인자→같은 int 추론→템플릿 후보 선택→필요한 정의 구체화와 본문 검사→실행 중 a 10, b 20 전달→20 반환으로 이어진다. 이는 설명용 의존 흐름이며 컴파일러 내부 패스 구현 순서를 뜻하지 않는다. 타입 추론 실패 간선은 별도의 10,3.14 호출로, 본문 검사 실패 간선은 비교를 지원하지 않는 별도 타입으로 향한다. 성공한 int 호출이 두 오류를 동시에 일으킨다는 의미가 아니다. 모바일에서도 같은 분기/성공 결과를 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 호출 준비 장부 두 인자의 타입으로 T의 일치 관계를 확인한다. |
|
|
| 컴파일·실행 경계 int 비교와 값 전달이 유효한 본문을 실제로 호출한다. |
|
|
| 반환값 본문의 비교가 더 큰 값을 고른다. |
|
|
함수 템플릿 호출은 추론부터 인스턴스 생성까지 이어진다
본문 첫 예제의 max(10,20)에서 T=int의 관계·정의의 int 비교/값 복사 적합성·실행 a 10/b 20·반환 20을 추적할 수 있다. 컴파일러 내부 pass 순서가 아닌 호출 준비 장부와 실행 경계를 표시하며 10/3.14 추론 실패와 비교 미지원 타입 오류는 별도 입력으로 둔다.
C++ · FUNCTION TEMPLATE CALL
max(10, 20)는 추론과 후보 선택을 거쳐 호출된다
본문 첫 예제의 template<typename T> T max(T a, T b)를 기준으로 읽습니다. 템플릿 이름 하나가 모든 타입의 연산을 보장하지는 않습니다.
- 호출 인자 확인
max(10, 20)에는 두 int 인자가 전달됩니다. - 템플릿 인수 추론
두 위치에서
T = int로 일치합니다.max(10, 3.14)는 int와 double 추론이 충돌합니다. - 사용 가능한 후보 선택
이름 탐색, 추론 결과, 인자 변환, 제약이 있다면 그 제약을 확인하여 가장 적합한 후보를 고릅니다.
- 선택된 함수 본문 검사
선택된 타입이
a > b와 값 복사 등 본문에서 사용한 연산을 지원해야 합니다. - 필요한 특수화 인스턴스화
해당 템플릿이 선택되면
max<int>의 정의가 필요에 따라 인스턴스화되고 결과는 20입니다.
오버로드가 추가되면: 본문 후반의 int max(int, int)처럼 다른 조건이 동등한 비템플릿 후보가 선택될 수 있습니다. max<int>(10, 20)은 템플릿 인수를 명시합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 8개 노드와 7개 간선이다. 첫 예제의 max(10,20)은 호출 인자→같은 int 추론→템플릿 후보 선택→필요한 정의 구체화와 본문 검사→실행 중 a 10, b 20 전달→20 반환으로 이어진다. 이는 설명용 의존 흐름이며 컴파일러 내부 패스 구현 순서를 뜻하지 않는다. 타입 추론 실패 간선은 별도의 10,3.14 호출로, 본문 검사 실패 간선은 비교를 지원하지 않는 별도 타입으로 향한다. 성공한 int 호출이 두 오류를 동시에 일으킨다는 의미가 아니다. 모바일에서도 같은 분기/성공 결과를 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 호출 준비 장부 두 인자의 타입으로 T의 일치 관계를 확인한다. |
|
|
| 컴파일·실행 경계 int 비교와 값 전달이 유효한 본문을 실제로 호출한다. |
|
|
| 반환값 본문의 비교가 더 큰 값을 고른다. |
|
|
왜 함수 템플릿을 사용하는가?
일반적으로 함수는 특정 데이터 타입에 대해 동작합니다.
예를 들어 두 정수를 더하는 함수와 두 실수를 더하는 함수는 다음과 같이 별도로 정의해야 합니다.
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)를 사용하여 정의합니다.
타입 매개변수는 실제 데이터 타입이 들어갈 자리임을 컴파일러에게 알려줍니다. 다음 형식의 반환타입·함수이름은 문법 자리를 설명하는 표시이며 그대로 컴파일하는 코드가 아닙니다. 바로 뒤의 max 예제는 별도 파일 하나로 실행할 수 있습니다.
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)라고 합니다.
각 호출의 인자 타입과 명시적 템플릿 인수가 어떤 특수화를 선택하는지 확인해 봅시다. max(10, 20)은 두 매개변수 자리에서 같은 int가 추론됩니다. max(10, 3.14)은 한 자리에서 int, 다른 자리에서 double을 요구하므로 추론이 실패합니다. 반환값을 double 변수에 넣겠다는 계획이 이 충돌을 해결해 주지는 않습니다. 반면 max<double>은 T를 먼저 고정하므로 10이 double로 변환된 뒤 비교합니다.
예시 다음 코드를 새 파일에 저장하고 C++17로 실행하세요. 앞 예제의 max와 함께 붙이지 않아도 되는 완전한 프로그램입니다.
#include <iostream>
template <typename T>
T pick_max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << pick_max<double>(2, 3.9) << '\n';
std::cout << pick_max<int>(2, 3.9) << '\n';
}출력은 차례로 3.9와 3입니다. 두 번째 호출은 반환 이후에만 정수로 바꾸는 것이 아니라, 함수에 들어갈 때 이미 b가 3입니다. 이 예제의 실수→정수 변환은 소수 부분을 버립니다. 값의 범위를 벗어나는 변환까지 안전하다고 일반화하지 마세요.
스스로 확인하기
두 인자를 4와 4.8로 바꾸면 위 두 줄의 출력은 무엇일까요? 이어 <double>을 지워 pick_max(4, 4.8)로 호출하면 큰 값을 출력할까요, 컴파일 단계에서 멈출까요? 각 호출을 별도로 확인하세요.
타입 고정과 추론의 차이 풀이
명시한 double 호출은 4.8, int 호출은 4입니다. int 매개변수에는 4.8이 4로 들어갑니다. 명시적 인수를 지운 호출은 int와 double의 T 추론이 충돌하여 컴파일되지 않으므로 그 호출의 실행 출력도 없습니다. 오류를 읽을 때 인수 추론 단계와 선택된 본문의 연산 검사를 구분합니다. 예를 들어 비교 연산을 정의하지 않은 사용자 타입 두 개는 T를 같은 타입으로 추론할 수 있어도, 이 본문의 a > b를 만들 수 없어 실패합니다.
명시적 템플릿 인수 지정
대부분의 경우 컴파일러는 함수 호출 시 전달된 인자의 타입을 통해 템플릿 인수를 자동으로 추론할 수 있습니다.
하지만 때로는 명시적으로 템플릿 인수를 지정해야 할 때가 있습니다.
컴파일러가 타입을 추론할 수 없을 때- 함수 인자에 템플릿 타입이 포함되지 않는 경우.
- 여러 개의 타입이 혼합되어 컴파일러가 하나의
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;
}create_zero()에는 T를 알아낼 함수 인자가 없습니다. double z = create_zero();처럼 받는 변수의 타입을 써도 이 호출의 T는 추론되지 않습니다. create_zero<double>()로 고정해야 합니다. 한편 flexible_max의 두 타입은 따로 추론되고, 이 int·double 예제에서는 조건 연산식의 결과 타입이 double입니다. 두 타입을 따로 받는다는 사실만으로 모든 사용자 타입의 비교나 공통 결과 타입이 보장되지는 않습니다.
함수 템플릿의 오버로딩
일반 함수처럼 함수 템플릿도 오버로딩될 수 있습니다.
즉, 동일한 이름의 템플릿 함수를 여러 개 정의할 수 있으며, 이들은 매개변수의 개수나 타입에 따라 구별됩니다.
#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;
}컴파일러는 함수를 호출할 때 오버로딩된 함수와 템플릿 함수 중에서 가장 적합한 것을 선택합니다.
먼저 이름 탐색과 템플릿 인수 추론으로 후보를 구성하고, 인자 변환의 우열과 템플릿 부분 순서 등으로 최적 후보를 고릅니다. 다른 조건이 동등할 때 비템플릿 함수를 선호합니다. 명시적 템플릿 인수 지정은 별도의 “두 번째 우선순위”가 아니라 템플릿 후보를 지정하는 호출 형태입니다.
세 인자 예시의 두 인자 재귀 호출은 템플릿 정의 시 보이는 앞선 함수 템플릿을 사용하므로 내부 호출 안내도 출력합니다. 주석의 최댓값만 출력되는 예시는 아닙니다. 여기의 int·double 인수에는 사용자 타입의 추가 이름 탐색 경로가 없으므로, 뒤에 선언한 int 일반 함수를 내부 호출이 새로 선택하지 않습니다.
예시
위 main의 max(10, 20, 30) 한 호출만 보면 다음 네 줄이 이어집니다. 바깥 호출이 시작된 뒤 안쪽에서 비교한 10·20의 결과 20을 다시 30과 비교합니다.
세 T 타입 인자를 받는 max 호출
두 T 타입 인자를 받는 max 호출
두 T 타입 인자를 받는 max 호출
30같은 예제의 main에서 기존 호출 대신 max<>(10, 20) 한 번만 출력해 보세요. 빈 꺾쇠도 템플릿 호출을 지정하므로 T를 int로 추론한 두 인자 템플릿의 안내 한 줄 뒤에 20이 나옵니다. 원래의 max(10, 20)은 다른 조건이 동등한 일반 함수의 안내 뒤에 20이 나옵니다. 값이 같아도 선택한 함수가 같은 것은 아닙니다.
함수 템플릿의 장점과 한계
장점- 코드 재사용성: 동일한 로직을 다양한 타입에 대해 재사용할 수 있어 코드 중복을 줄입니다.
- 타입 안전성: 템플릿은 컴파일 시점에 타입을 결정하므로, 런타임 오류가 아닌 컴파일 타임 오류를 통해 타입 불일치를 잡아냅니다.
- 성능: 필요한 타입으로 함수 정의가 구체화되어 정적 호출과 최적화에 유리할 수 있습니다. 하지만 값 복사와 함수 본문의 연산 비용이 사라지는 것은 아닙니다. 위
max에서std::string인수를 값으로 받는 부분도 복사 비용을 고려해야 합니다. 실제 속도는 타입·구현·빌드 설정에 따라 달라집니다.
- 컴파일 시간 증가: 템플릿은 사용되는 모든 타입에 대해 코드를 생성하므로, 컴파일 시간이 길어질 수 있습니다.
- 코드 크기 증가 (Code Bloat): 사용되는 타입의 조합만큼 코드가 생성되므로, 실행 파일의 크기가 커질 수 있습니다.
- 디버깅 어려움: 템플릿 오류 메시지는 종종 복잡하고 이해하기 어려울 수 있습니다.
- 모든 연산이 가능해야 함: 템플릿 함수 내부에서 사용되는 연산(예:
a > b,a + b)은T타입이 해당 연산을 지원해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.
함수 템플릿은 “중복을 줄이는 도구”이면서 동시에 “컴파일러가 타입별 함수를 생성하게 하는 약속”입니다.
사용 타입이 본문 연산을 지원하는지와 인스턴스 증가 비용을 함께 점검합니다.
호출 시 점검
마지막으로 호출문을 볼 때 확인할 세 가지 축을 정리합니다.
함수 템플릿은 T 추론, 명시적 인수 지정, 오버로딩의 우선순위를 함께 읽어야 합니다.
이 세 요소를 구분하면 추론 실패와 후보 선택 실패의 원인을 좁힐 수 있습니다. 호출이 컴파일되지 않으면 먼저 T를 한 조합으로 결정할 수 있는지, 그다음 호출 가능한 후보가 유일하게 선택되는지, 마지막으로 선택한 본문에서 필요한 연산을 제공하는지를 따져 보세요. 함수 인자로부터의 타입 추론 규칙과 필요한 정의의 암시적 인스턴스화는 이 단계들의 조건을 구분합니다.