상세 컨텐츠

본문 제목

C++ 함수 템플릿

C++/C++98

by deulee 2023. 8. 8. 14:01

본문

함수 템플릿이란 뭘까?

 

템플릿(Template)의 사전적 정의는 무엇인가를 만들기 위한 형틀이라는 뜻이다.

 

즉, 틀은 같은데 집어 넣는 입력에 따라 결과물들이 조금씩 달라진다는 것이다.

 

1. 함수 템플릿

함수 템플릿은 다음과 같은 상황에서 만들어졌다고 생각하면 된다.

void swap(int &a, int &b)
{
	int t;
	t = a;
	a = b;
	b = t;
}

void swap(double &a, double &b)
{
	double t;
	t = a;
	a = b;
	b = t;
}

이 두 함수를 보면 하는 동작 즉, 틀은 똑같은데 매개 변수로 받는 타입(재료)만 다른 것이 보이는가?

 

즉, 굳이 각 타입별로 함수를 새로 만들어야 할까?

 

이를 대안해서 나온 방법이 메크로를 사용한다던가, 포인터(void *)를 이용한다던가 타입 지정자를 이용한다던가 말이다.

 

하지만 하나같이 단점이 존재하고 이를 보완해서 만들어진 개념이 바로 템플릿이다.

 

함수 템플릿의 원형은 다음과 같다.

template <typename T> // 함수 템플릿의 정의 부분이다. 뒤의 호출부 <typename T>에 T는 전달된 타입이다.
T Function(T a, T b)
{
	T ...; // 함수의 본체에서 언제든지 T를 참조할 수 있다.
}

template <class T> // 이것도 가능

함수 호출부에서 int를 사용하면 "T"는 int가 되며 함수 본체에서 참조하는 "T"는 모두 int가 될 것이다. 즉, 호출부에서 전달되는 실제 타입을 템플릿 정의에서 표기하기 위한 임시적인 이름이 바로 "typename T"이다. 

 

그럼 이를 이용하여 예시를 만들어 보도록 하겠다.

#include <iostream>

template <typename T>
void swap(T& a, T& b)
{
	T t;
	t = a;
	a = b;
	b = t;
}


int main(void)
{
	int a = 3, b = 4;
	double c = 2.2, d = 7.4;

	swap(a, b);
	std::cout << a << b << std::endl;
	swap(c, d);
	std::cout << c << d << std::endl;
	return 0;
}

만약 위의 예제에 정수나 실수를 제외하고 문자열, 구조체를 넣어도 임의의 타입에 대해 Swap 함수는 정상적으로 작동할 것이다.

 

그럼 매크로 함수랑은 무엇이 다를까?

 

매크로 함수는 전처리기가 처리하지만 템플릿은 컴파일러가 직접 처리한다. 전처리기는 지시대로 소스를 재구성할 뿐이므로 개발자가 필요한 타입에 대해 일일이 매크로를 전개해야 하므로 수동이지만 템플릿은 호출만 하면 컴파일러가 알아서 함수를 만드는 자동식이므로 매크로 함수보다는 더 좋다.

 

또한 함수 본체에서 변화가 생길만한 타입이 둘 이상이라면 함수 템플릿도 여러 개의 인수를 가질 수 있다.

template <typename T1, typename T2>

2. 구체화

함수 템플릿은 어디까지나 함수를 만들기 위한 형틀이지 함수가 아니다.

 

함수 템플릿으로부터 함수를 만드는 과정구체화 혹은 인스턴스화(Instantiation)이라고 하는데 "호출에 의해 구체화되"어야만 실제 함수가 만들어진다.

 

즉, 존재하는 모든 타입에 대해 미리 함수를 만들어 놓는 것이 아니니 주의를 하도록 하자.

  • 함수 템플릿 - 함수를 만드는 템플릿
  • 템플릿 함수 - 템플릿으로부터 만들어지는 함수

템플릿을 정의하고 함수를 호출하지 않으면 아무런 일도 일어나지 않으며 템플릿 자체는 메모리를 소모하지 않는다.

 

그렇다고 템플릿 함수가 런타임에서 구현되는 것은 아니며 "컴파일러"에 의해 호출된 타입 만큼 구체화를 시키며 실행시의 부담은 전혀 없다. 대신 매 타임마다 함수들이 새로 만들어지므로 구체화되는 수 만큼 실행 파일의 용량이 늘어난다.

 

즉, 속도와 크기는 반비례한다.

 


3. 명시적 인수 지정

컴파일러는 호출부의 실인수 타입을 판별하여 필요한 함수를 구체화하는데, 이때 위의 예제에서는 템플릿 타입 정의에 의해 두 인수의 타입은 같아야 하므로 서로 다른 타입이 인수로 들어오면 에러를 처리한다.

 

템플릿은 타입을 암시적 변환해서 호출할 수 없다, 즉 (int, double)을 암시적으로 (double, double)로 변환시키지 않는다는 것이다.

 

그렇기 때문에 리턴 타입이나 매개 변수로 직접 사용되지 않는 타입을 가지는 함수를 호출하기 위해서는 명시적으로 템플릿의 인수 타입을 지정해야 한다.

template <typenmae T>
T cast(int s)
{
	return (T)s;
}

template <typename T>
void func(void)
{
	T v;

	std::cin >> v;
	std::cout << v;
}

int main(void)
{
	double a = cast<double>(1);

	func<int>();
	return 0;
}

위의 예제 처럼 "<>"를 이용하여 어떤 타입의 템플릿 함수를 구체화할 것인지 명시적으로 지정할 수 있다.

 

위의 예시처럼 리턴 타입이나 내부에서 지역 변수로 사용할 때 즉, 매개 변수 등으로 타입을 지정할 수 없는 경우 매우 유용하다.

 


4. 명시적 구체화

지금까지는 함수의 호출부를 보고 컴파일러가 템플릿 함수를 알아서 만드는 것을 암시적 구체화라고 한다. 즉, 개발자가 특정 타입에 대해 호출하지 않는 이상 해당 타입의 함수는 구체화가 되지 않는 것이다.

 

그렇다면 강제로 해당 타입에 대해서 템플릿 함수를 구체화 하고 싶을 때 사용하는 것이 명시적 구체화(Explicit Instantiation)다.

 

명시적 구체화의 정의는 다음과 같이 한다.

template void Swap<float>(float, float);

위의 명시적 구체화는 템플릿 선언이 먼저 되어있다는 가정하에 작동한다.

 

이렇게 미리 사용할 함수들을 정해 놓는다면 컴파일 시간을 절약할 수 있다.

 

여담

추가로 템플릿도 인수 목록이 다르면 오버라이딩이 가능하다.

template <typename T>
void swap(T& a, T& b)
{
	T t;
	t = a;
	a = b;
	b = t;
}

template <typename T>
void swap(T& a, T& b, T& c)
{
	T t = a;
	a = b;
	b = c;
	c = t;
}

5. 주의해야 할 점

함수 템플릿의 본체 코드는 임의의 타입에 대해서도 동일하게 동작하므로 타입에 종속적인 코드는 사용할 수 없다.

 

이 말이 무슨 말일까?

template <typename T>
void printValue(T value)
{
	printf("value is %d\n", value);
}

위의 코드는 템플릿이기 때문에 어떠한 임의의 타입을 받을 수 있지만 본체를 보면 "%d" 서식을 이용하고 있어 정수 타입에서만 정상적으로 작동할 것이다.

 

또한, 저 임의의 타입으로 구조체나 클래스를 넣어도 컴파일은 될 것이다.

 

하지만 그로 인해 런타임에서 에러가 나는 것은 컴파일러가 책임지지 않는 것이다.

 

 컴파일러는 구체화된 템플릿 함수에 대해서만 에러 체크를 할 뿐이지 템플릿 자체에 대해서는 상세한 점검을 하지 않는다. 즉, 함수 템플릿 본체에 "if + while;" 같은 말도 안되는 구문도 컴파일이 될 것이다.

 

그렇기 때문에 템플릿을 작성할 때는 본체에 전달될만한 타입을 모두 지원하는 범용적인 코드만 작성해야 한다.

 

 

'C++ > C++98' 카테고리의 다른 글

C++ 예외 처리  (0) 2023.08.08
C++ 클래스 템플릿  (0) 2023.08.08
C++ 순수 가상 함수  (0) 2023.08.07
C++ 가상 파괴자  (0) 2023.08.07
C++ 가상 함수  (0) 2023.08.07

관련글 더보기