개발하는 리프터 꽃게맨입니다.

[C++] 템플릿 기본 본문

언어/C, C++

[C++] 템플릿 기본

파워꽃게맨 2024. 1. 15. 12:57

개론

템플릿 문법은 일반화 프로그래밍에 사용되는 문법입니다.

C++의 컨테이너, 반복자, 알고리듬 모두 일반화 프로그래밍의 일종이죠

 

일반화 프로그래밍이란,

'일반화 정의'를 사용해 다양한 타입의 형태를 만들어서 사용할 수 있는 것을 뜻합니다.

 

C의 void* 문법과 비슷하다고 볼 수 있으나

템플릿 문법은 훨씬 사용하기 쉽고 다양한 기능을 제공합니다.

(템플릿 문법이 사용하기는 쉽지만, 매우 다양한 문법을 지원합니다. 그래서 깊이 파면 공부할 양이 상당히 많은

문법입니다.)

 

흔히, 템플릿은 '청사진을 만든다.'라고 비유하기도 하는데요.

말 그대로 청사진을 만들어서 원하는 형태로 변형시켜서 사용할 수 있습니다.

이러한 변형은 컴파일 시간에 발생합니다.

 

함수 템플릿과 클래스 템플릿

1. 함수 템플릿

a, b를 print 하는 함수가 있다고 가정합니다.

만약, 다양한 형식의 매개변수를 처리하려면 오버로딩을 통해 처리할 수 있습니다.

 

그러나 템플릿 문법을 사용하면

단 하나의 일반화 함수를 만들어 처리할 수 있으며

컴파일 타임에 컴파일러가 인자들을 추론하여, 여러 가지 버전의 함수를 인스턴스화합니다.

위 2개의 사진에 있는 함수는

완전히 동일한 기능을 수행합니다.

 

물론 아래 버전이 훨씬 더 많은 종류의 데이터 타입을 수용할 수 있죠.

 

사용하는 방법은

template <템플릿 매개변수 목록>

을 함수 앞에 붙여주기만 하면 됩니다.

 

템플릿 매개변수는 쉼표로 구분하여 다중 정의할 수 있고

반드시 하나 이상의 템플릿 매개변수가 존재해야 합니다.

 

참고로, 템플릿 매개변수를 정의할 때 typename 혹은 class를 사용하는데

어떤 것을 사용해도 상관없으나 보통 전자를 많이 사용합니다.

 

템플릿 매개변수는 함수 매개변수와 동일하게 행동하며, 실제 타입은 컴파일 시점에서 결정됩니다.

 

 

사용법은 위와 같습니다.

명시하지 않아도 컴파일러가 알아서 데이터 형을 추론하며 (만약 모호할 경우 에러가 납니다.)

명시적으로 컴파일러에게 인스턴스화 형태를 지정해 줄 수도 있습니다.

 

2. 클래스 템플릿

클래스 템플릿은 함수 템플릿을 이해하였다면 위화 감 없이 사용할 수 있습니다.

'특정 자료형을 저장하는 배열'을 관리하는 class가 있다면,

다음과 같이 템플릿으로 정의할 수 있습니다.

 

 

template로 정의한 class의 모습입니다.

T라는 임시 데이터 형을 가지고 있고, 나중에 데이터형을 구체적으로 정해준다라고 이해하면 좋습니다.

 

 

템플릿 함수와 다르게 어떤 데이터 형을 바인딩해야 하는지, 컴파일러는 추론할 수 없습니다.

그렇기에 템플릿 클래스의 경우 데이터 형을 명시해줘야 합니다.

 

3. 주의할 점

1) 분할구현 금지

템플릿 문법의 선언과 정의는 반드시 하나의 파일에 존재해야 하며, 분할구현이 불가능하다는 것입니다.

보통 함수의 선언과 구현체를. h,. cpp에 분할하는 경우가 많은데, 템플릿의 경우 이런 방식으로 사용하면 알 수 없는 외부 참조라는 오류가 발생합니다.

 

2) 템플릿의 생명주기

템플릿의 생명주기는 하나의 클래스, 하나의 함수의 시작과 끝입니다.

하나의 클래스와 함수마다 템플릿 구문을 하나하나 추가해줘야 합니다. 

 

비타입 템플릿 매개변수

말 그대로 타입이 아닌 값을 정의하는 템플릿 매개변수입니다.

typename으로 임시 데이터 형을 정의해 주는 기존 템플릿 문법과 다르게

명확한 데이터 형식을 정의해 줍니다.

 

 

템플릿 구문에..., size_t N 가 추가되었습니닫.

이것은 템플릿 클래스를 인스턴스화할 때, N 또한 정의해 준다는 의미이며

N은 해당 템플릿 클래스, 함수 내에서 상수처럼 사용됩니다.

 

위 코드는 '배열'의 크기를 정의해주고 있는 모습입니다.

 

이런 식으로

데이터 형을 명시하듯 비타입 템플릿 매개변수를 명시하여 사용합니다.

 

아마 위 코드 대로라면 크기 30의 int 형 배열이 생성되겠죠?

템플릿 특수화

마치 오버로딩과 비슷한 개념입니다.

일반화 클래스, 함수를 만들어서 모든 데이터형에 대해서 일반화 코드가 작동하도록 구현했는데,

'특정 데이터 형'에 대해서는 다른 방식의 작동을 원할 경우

따로 구현해 주는 것이 템플릿 특수화입니다.

 

예를 들어 bool 배열을 생각해 봅니다.

bool은 0, 1 만을 나타낼 수 있으면 됩니다.

단, 1비트만 있으면 되는데, 아쉽게도 C++에서는 최소 데이터 단위가 1byte입니다.

MyVector 템플릿 클래스로 bool을 선언하면 100byte의 메모리를 소모합니다.

 

사실은 100개의 bool을 관리하려면 13byte (= 104bit) 면 충분하죠?

 

이럴 경우 템플릿 특수화를 이용해서 bool 전용 템플릿 클래스를 재정의 해줍니다.

이런 식으로 데이터 형을 나타내는 typename을 아예 없애고

class MyArray <bool, N> {... }

이런 형식으로 계속해서 정의해 주면 되겠습니다.

 

(클래스 내부는 굳이 구현하지 않았습니다. bit 연산에 대한 지식만 있으면 쉽게 할 수 있을 겁니다.)

 

이것이 템플릿 특수화입니다.

더보기

<전체코드>

template <typename T, size_t N>
class MyArray
{
public:
	MyArray() = default;
	~MyArray() = default;

	/* ..추가적인 기능 생략.. */

	T& operator[](size_t idx)
	{
		assert(idx >= 0 && idx < _size);

		return _array[idx];
	}

	void push_back(T item)
	{
		assert(!full());
		_array[_size] = item;
		_size++;
	}

	bool full() const { return _size == _capacity; }
	bool empty() const { return _size == 0; }

private:

	T		_array[N];
	size_t	_size = 0;
	size_t	_capacity = N;
};

template <size_t N>
class MyArray<bool, N>
{
public:
	MyArray() = default;
	~MyArray() = default;

	/* ..추가적인 기능 생략.. */

	bool& operator[](size_t idx)
	{
		assert(idx >= 0 && idx < _size);

		/*내부 구현 굳이 X*/

		return false;
	}

	void push_back(bool item)
	{
		assert(!full());
		
		/*내부 구현 굳이 X*/
	}

	bool full() const { return _size == _capacity; }
	bool empty() const { return _size == 0; }

private:
	enum { MAX = N % 8 + 1 };

	bool	_array[MAX];
	size_t	_size = 0;
	size_t	_capacity = N;
};

추가적인 유용한 문법

1. 함수 객체의 도입

 

템플릿 매개변수로 데이터 형뿐만 아니라, 함수도 받아줄 수 있습니다.

이러면 함수를 쉽게 매개변수로 넘길 수 있습니다.

 

C의 함수 포인터와 유사한 방법인데,

단순하게 함수만 넘겨주는 것이 아닌, 이런 식으로 struct, class를 넘겨주게 되면

멤버 변수까지 사용할 수 있다는 어마무시한 장점이 존재합니다.

 

이것은 다른 포스팅에서 자세히 설명하도록 하겠습니다.

 

2. 디폴트 템플릿 인자

디폴트 템플릿 인자는 템플릿 매개변수에 디폴트 값을 넣어줄 수 있다는 것입니다.

 

다시 한번 MyArray 클래스를 가져왔습니다.

위 템플릿 구문을 보면 <typename T = int>라고 적혀있습니다.

 

이럴 경우, 인스턴스 생성 시 데이터 타입을 명시하지 않았을 경우 자동적으로 int 형식 템플릿 인스턴스가 생성됩니다.

 

 

이제 이 정도 템플릿 지식이 있으면

템플릿 프로그래밍의 결과물인 STL 컨테이너를 분석해 보면 많은 부분을 이해할 수 있습니다.

 

 

STL vector입니다.

vector은 _Ty로 데이터 타입을 템플릿 매개변수로 받아주고 있으며

잘은 모르겠지만 _Alloc라는 매개변수를 하나 더 받아주고 있고, allocator <_Ty>이라는 디폴트 값을 사용하고 있습니다.

 

 

왼쪽 화살표를 내리면

bool 형식만을 위한 '특수화 템플릿'이 존재하는 것을 관찰할 수 있습니다.

마치며

템플릿 문법은 매우 유용하고, 실제로도 많이 사용합니다.

그러나 남용에는 문제가 있죠.

 

템플릿 프로그래밍을 하면 컴파일 시간에 컴파일러가 템플릿 매개변수를 추론하여

다양한 함수를 인스턴스화합니다.

그렇기 때문에 템플릿 프로그래밍 시 컴파일 속도가 느려질 가능성이 있죠.

 

그리고 일반화 코드이기 때문에 읽기 어려운 것, 다양한 템플릿 인자를 사용할수록 절대적인 코드의 양이 늘어나서 런타임 속도에도 영향을 미칠 수 있다는 것이 단점입니다.

 

그래서 정말 필요한 곳만 짧게 템플릿 문법을 사용하는 것을 권장하여

2개 정도의 오버로딩이 예상된다고 한다면 차라리 inline 함수를 오버로딩 하여 사용하는 것이 더 좋을 수 있습니다.

 

그러나 컨테이너, 4개 이상의 자료형을 다루는 함수, 클래스라면 템플릿 문법은 매우 유용하겠죠?

 

템플릿 문법은 아직 저도 기초밖에 다루지 못해서 다음에 더 공부해서 새로운 포스팅으로 찾아뵙겠습니다.

 

참고로 템플릿 문법 실제로 사용하다 보면 헷갈리는 부분이 상당히 많습니다.

이 부분은 많은 실습을 통해서 익숙해지셔야 해요