함수 템플릿
하나의 함수 정의를 여러 타입에 재사용하는 템플릿 문법과 타입 추론·명시적 인수·오버로딩 규칙을 익힙니다.
두 수 가운데 큰 값을 돌려주는 함수를 int용, double용, char용으로 만들면 본문은 모두 return (a > b) ? a : b;로 같고 타입만 다릅니다. 함수 오버로딩으로 타입마다 함수를 늘리면 같은 코드가 반복되고, 고칠 때도 모두 고쳐야 합니다. 템플릿(template)은 타입을 매개변수로 받아, 이런 함수를 한 번만 정의하게 해 줍니다.
이 장에서는 타입에 상관없이 쓰는 코드를 만드는 템플릿을 다룹니다. 이 글은 함수 템플릿을 정의하고 호출하는 방법, 컴파일러가 타입을 추론하는 규칙, 타입을 직접 지정하는 방법, 함수 템플릿의 오버로딩을 봅니다.
함수 템플릿의 정의와 호출
함수 템플릿은 template <typename T>로 시작하며, T는 실제 타입이 들어갈 자리인 타입 매개변수(type parameter)입니다. typename 대신 class를 써도 같은 뜻입니다.
#include <iostream>
#include <string>
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max(10, 20) << '\n';
std::cout << max(3.14, 2.71) << '\n';
std::cout << max('A', 'Z') << '\n';
std::cout << ::max(std::string("apple"), std::string("banana")) << '\n';
std::cout << max<double>(10, 3.14) << '\n';
}20
3.14
Z
banana
10max(10, 20)을 호출하면 컴파일러는 두 인자가 모두 int이므로 T를 int로 추론하고, int max(int, int) 함수를 만들어 호출합니다. 이렇게 템플릿에서 실제 타입의 함수를 만드는 것을 템플릿 인스턴스화(template instantiation)라고 하며, 실행 중이 아니라 컴파일할 때 일어납니다. 아래 그림은 이 호출이 추론, 후보 선택, 본문 검사를 거쳐 20을 돌려주기까지의 흐름입니다.
함수 템플릿 호출은 추론부터 인스턴스 생성까지 이어진다
본문 첫 예제의 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 비교와 값 전달이 유효한 본문을 실제로 호출한다. |
|
|
| 반환값 본문의 비교가 더 큰 값을 고른다. |
|
|
그림의 갈래처럼 실패하는 경우는 두 가지입니다. max(10, 3.14)는 한 인자에서 int, 다른 인자에서 double이 추론되어 T를 하나로 정할 수 없으므로, 추론 단계에서 컴파일 오류가 납니다. 반면 > 연산이 없는 타입 두 개를 넘기면 T는 정해지지만, 본문의 a > b를 만들 수 없어 본문을 검사하는 단계에서 오류가 납니다. 템플릿은 타입 이름을 매개변수로 바꿀 뿐, 넘긴 타입이 본문의 연산을 지원해야 쓸 수 있습니다.
문자열 호출에 ::max라고 쓴 것은, std::string 인자 때문에 인자 의존 조회로 표준 라이브러리의 std::max도 후보가 되어 호출이 모호해지기 때문입니다. ::를 붙이면 전역 이름 공간의 max만 찾습니다. 이런 충돌을 피하려고 실제 코드에서는 직접 만든 함수에 표준 라이브러리와 겹치지 않는 이름을 쓰거나, std::max를 그대로 씁니다.
템플릿 인수를 직접 지정하기
max<double>(10, 3.14)처럼 함수 이름 뒤에 꺾쇠괄호로 타입을 적으면, 추론하지 않고 T를 그 타입으로 정합니다. 이것을 명시적 템플릿 인수(explicit template argument)라고 합니다. T가 double로 정해지면 10은 double로 변환되어 들어가므로, 앞의 추론 충돌이 사라지고 큰 값 10이 출력됩니다.
#include <iostream>
template <typename T>
T pickMax(T a, T b) {
return (a > b) ? a : b;
}
template <typename T>
T createZero() {
return static_cast<T>(0);
}
int main() {
std::cout << pickMax<double>(2, 3.9) << '\n';
std::cout << pickMax<int>(2, 3.9) << '\n';
std::cout << createZero<double>() << '\n';
// int z = createZero(); // 컴파일 오류: T를 추론할 수 없음
}3.9
3
0pickMax<int>(2, 3.9)는 함수에 들어갈 때 이미 3.9가 int인 3으로 바뀌므로, 3을 돌려줍니다. 결과를 돌려준 뒤에 정수로 바꾸는 것이 아니라 매개변수부터 int라는 점에 주의합니다. createZero()처럼 매개변수에 T가 없는 함수는 추론할 단서가 없으므로 반드시 타입을 지정해야 하며, 결과를 받는 변수의 타입은 추론에 쓰이지 않습니다.
두 인자의 타입이 달라도 되게 하려면 타입 매개변수를 둘로 나눕니다.
#include <iostream>
template <typename T1, typename T2>
auto flexibleMax(T1 a, T2 b) {
return (a > b) ? a : b;
}
int main() {
std::cout << flexibleMax(5, 3.14) << '\n';
}5T1은 int, T2는 double로 각각 추론되고, 반환 타입을 auto로 두면 C++14부터 return 식의 타입으로 정해집니다. 조건 연산자의 결과 타입은 두 피연산자의 공통 타입인 double이므로 5.0이 돌아오고, 기본 출력 형식으로 5가 출력됩니다.
함수 템플릿의 오버로딩
함수 템플릿도 일반 함수처럼 오버로딩할 수 있으며, 같은 이름의 일반 함수와 함께 둘 수도 있습니다. 아래 코드에는 두 인자 템플릿, 세 인자 템플릿, int 두 개를 받는 일반 함수가 함께 있고, 각 함수는 불릴 때 자기 이름을 먼저 출력합니다. 그래서 출력에서 각 결과값 위의 줄들을 보면, main의 네 호출이 저마다 어느 함수로 정해졌는지 알 수 있습니다.
#include <iostream>
template <typename T>
T max(T a, T b) {
std::cout << "template max(a, b)\n";
return (a > b) ? a : b;
}
template <typename T>
T max(T a, T b, T c) {
std::cout << "template max(a, b, c)\n";
return max(max(a, b), c);
}
int max(int a, int b) {
std::cout << "plain int max\n";
return (a > b) ? a : b;
}
int main() {
std::cout << max(10, 20) << '\n';
std::cout << max(3.14, 2.71) << '\n';
std::cout << max(10, 20, 30) << '\n';
std::cout << max<>(10, 20) << '\n';
}plain int max
20
template max(a, b)
3.14
template max(a, b, c)
template max(a, b)
template max(a, b)
30
template max(a, b)
20max(10, 20)은 템플릿에서 만든 max<int>와 일반 함수 max(int, int)가 모두 정확히 맞는데, 다른 조건이 같으면 일반 함수가 우선하므로 일반 함수가 호출됩니다. max(3.14, 2.71)은 double을 받는 일반 함수가 없어 템플릿이 쓰입니다. max<>(10, 20)처럼 빈 꺾쇠괄호를 붙이면 템플릿만 후보로 삼으라는 뜻이어서, 같은 20이 나와도 다른 함수가 호출됩니다.
세 인자 호출 안의 max(max(a, b), c)가 일반 함수가 아니라 두 인자 템플릿을 부르는 것은, 템플릿을 정의한 시점에 보이는 이름만 찾기 때문입니다. 일반 함수 max(int, int)는 세 인자 템플릿보다 뒤에 선언되어 있고, int에는 인자 의존 조회로 더 찾을 곳도 없으므로 후보가 되지 않습니다. 그래서 두 인자 템플릿의 안내가 두 번 출력됩니다.
더 깊이: 두 단계 이름 찾기와 MSVC
템플릿 안의 이름은 두 단계로 찾습니다. 템플릿 매개변수와 상관없는 비의존 이름(non-dependent name)은 템플릿을 정의하는 시점에 찾아 고정하고, a > b의 operator>처럼 템플릿 인수에 따라 뜻이 달라지는 의존 이름(dependent name)은 인스턴스화할 때 인자 의존 조회를 더해 찾습니다. 이를 두 단계 이름 찾기(two-phase name lookup)라고 합니다. 의존하는 기반 클래스의 멤버도 정의 시점에는 찾지 않으므로, 클래스 템플릿에서 그런 멤버를 부를 때는 this->f()처럼 써서 의존 이름으로 만들어야 합니다.
MSVC는 /permissive- 옵션이 없으면 이 규칙을 다 따르지 않아, 의존하는 기반 클래스의 멤버를 this-> 없이 부르는 코드처럼 GCC와 Clang이 거부하는 코드를 받아들일 수 있습니다. Microsoft 문서에 따르면 Visual Studio 2017 버전 15.3부터 /permissive-를 켜면 템플릿 정의를 해석해 의존 이름과 비의존 이름을 나누며, 15.7부터는 정의 시점의 인자 의존 조회까지 합니다. Visual Studio 2019 버전 16.11부터는 /std:c++20을 주면 /permissive-도 함께 켜지고, 옛 동작이 필요한 코드는 /Zc:twoPhase-로 이 부분만 되돌릴 수 있습니다. 그래서 MSVC에서만 빌드되던 템플릿 코드를 다른 컴파일러로 옮길 때는 이 차이에서 생긴 오류를 먼저 의심합니다.
장점과 주의점
함수 템플릿은 같은 코드를 한 번만 쓰게 하고, 타입 검사가 컴파일할 때 이루어지므로 잘못된 타입은 실행하기 전에 오류가 납니다. 타입마다 함수가 만들어지므로 호출이 직접 호출이 되어 최적화에도 유리합니다. 다만 위의 max는 인자를 값으로 받으므로 std::string을 넘기면 복사가 일어나며, 큰 객체를 다룰 템플릿이라면 const T&로 받는 편이 낫습니다.
대신 쓰는 타입의 조합마다 함수가 만들어지므로 컴파일 시간과 실행 파일 크기가 늘 수 있고, 템플릿 안에서 난 오류는 메시지가 길고 읽기 어렵습니다. C++20에서는 콘셉트(concept)로 T가 갖춰야 할 조건을 선언에 적어 두어, 조건에 맞지 않는 타입을 넘기면 짧고 분명한 오류가 나게 할 수 있습니다. 콘셉트는 C++ 20 기능 소개에서 다룹니다.
정리
- 함수 템플릿은
template <typename T>로 타입을 매개변수로 받아, 여러 타입에 같은 코드를 한 번만 정의합니다. - 호출할 때 컴파일러가 인자의 타입으로
T를 추론하고 그 타입의 함수를 만들며, 이를 인스턴스화라고 합니다. - 인자마다 다른 타입이 추론되면 추론이 실패하고, 넘긴 타입이 본문의 연산을 지원하지 않으면 본문 검사에서 실패합니다.
f<double>(...)처럼 타입을 직접 지정하면 인자가 그 타입으로 변환되며, 매개변수에T가 없으면 반드시 지정해야 합니다.- 템플릿과 일반 함수가 똑같이 맞으면 일반 함수가 우선하며,
f<>(...)는 템플릿만 후보로 삼습니다. - 템플릿 안의 호출은 정의 시점에 보이는 이름과 인자 의존 조회로 찾으므로, 뒤에 선언한 함수는 후보가 되지 않을 수 있습니다.
연습 문제
1. 지정한 타입에 따라 달라지는 결과
explicit_argument.cpp에서 인자를 4와 4.8로 바꾸면 pickMax<double>과 pickMax<int>는 각각 무엇을 출력하나요? pickMax(4, 4.8)로 타입 지정을 빼면 어떻게 되나요?
해설 보기
pickMax<double>(4, 4.8)은 4.8, pickMax<int>(4, 4.8)은 4.8이 4로 바뀌어 들어가므로 4를 출력합니다. 타입 지정을 빼면 int와 double이 함께 추론되어 T를 정할 수 없으므로 컴파일 오류가 나고, 실행 결과도 없습니다.
2. 어느 max가 호출되는가
template_overload.cpp에서 max('A', 'Z')와 max(10, 'Z')는 각각 어느 함수를 호출하나요?
해설 보기
max('A', 'Z')는 T가 char로 추론되어 템플릿이 정확히 맞고, 일반 함수는 char를 int로 승격해야 하므로 템플릿이 호출되어 Z를 출력합니다. max(10, 'Z')는 템플릿의 추론이 int와 char로 엇갈려 실패하므로, 남은 일반 함수 max(int, int)가 'Z'를 90으로 승격해 호출되고 90을 출력합니다.
3. 비교 연산이 없는 타입
struct Point { int x; int y; };의 두 객체로 max(p1, p2)를 호출하면 어느 단계에서 실패하나요? max(p1, 3)은 어떤가요?
해설 보기
max(p1, p2)는 T가 Point로 정해지지만, 본문의 a > b에 쓸 > 연산자가 Point에 없어 본문을 인스턴스화하는 단계에서 오류가 납니다. max(p1, 3)은 Point와 int가 함께 추론되어 그보다 앞의 추론 단계에서 실패합니다. 오류 메시지를 읽을 때 어느 단계의 실패인지 먼저 구분하면 원인을 빨리 찾을 수 있습니다.
다음 글 클래스 템플릿에서는 std::vector처럼 타입을 매개변수로 받는 클래스를 정의하고, 템플릿의 정의를 헤더에 두어야 하는 이유를 다룹니다.