상세 컨텐츠

본문 제목

C++ 클래스 템플릿

C++/C++98

by deulee 2023. 8. 8. 15:41

본문

1. 타입만 다른 클래스들

클래스 템플릿이나 함수 템플릿은 비슷하되 찍어내는 대상이 함수가 아니라 클래스라는 것만 다르다.

 

다음과 같은 클래스를 보도록 하자.

 

class ValueInt
{
private:
	int x, y;
	int value; // 타입이 int
public:
	...
}

class ValueDouble
{
private:
	int x, y;
	double value; // 타입이 double
public:
	...
}

이렇게 위처럼 타입만 다르지 내용은 똑같은 클래스를 다음처럼 하나의 템플릿으로 통합할 수 있다.

 

#include <iostream>

template <typename T>
class Value
{
private:
	int x, y;
	T value; // 타입이 T
public:
	Value(int ax, int ay, T av) : x(ax), y(ay), value(av)
	{}
	void outValue();
};

template <typename T>
void Value<T>::outValue()
{
	std::cout << x << ' ' << y << std::endl;
	std::cout << "value : " << value << std::endl;
}

int main(void)
{
	Value<int> iv(1, 2, 3);
	Value<char> cv(5, 2, 'C');
	Value<double> dv(6, 2, 3.14);
	iv.outValue();
	cv.outValue();
	dv.outValue();
	return 0;
}

이렇게 클래스 템플릿으로부터 다양한 템플릿 클래스를 만들 수 있고 템플릿 클래스의 타입명에는 "< >" 괄호가 항상 따라다닌다.

 

클래스 템플릿의 멤버 함수를 선언문 외부에서 작성할 때는 템플릿에 속한 멤버 함수임을 밝히기 위해 소속 클래스의 "<T>"를 붙여야 하며 "T"가 인수임을 명시하기 위해 "template <typename T>"가 먼저 와야 한다.

 

내부에서 작성할 때는 이미 클래스 선언문 앞에 T에 대한 설명이 있기 때문에 또 명시할 필요가 없다.

 

물론 컴파일러가 객체 선언문에 있는 초기값의 타입으로 어떤 타입에 대한 클래스를 원하는지 유추할 수 있을 것 같지만 생성자가 오버로딩되어 있을 경우 이 정보만으로는 원하는 타입을 정확하게 판단할 수 없다.

 

그리고 생성자를 호출하기 전에 객체를 위한 메모리를 할당해야 하는데 이 시점에서 생성할 객체의 크기를 먼저 계산할 수 있어야 하므로 클래스 이름에 타입이 명시되어야 한다.

 

클래스 탬플릿도 그 자체로는 형태에 불과하므로 메모리가 할당되지 않으며 객체가 생성될 때만 메모리가 할당된다.

 

하지만 다음의 경우는 좀 다르다.

class ChildValue : public Value<int>
{...}

템플릿 클래스도 엄연한 클래스이기 때문에 다음과 같이 상속이 가능하다. 이런 경우 인스턴스를 생성하지 않더라도 해당 클래스를 즉시 구체화한다. (메모리에 할당)

 

또한 멤버 함수도 함수이기 때문에 타입에 따라 여러 벌이 필요하다면 원하는 멤버 함수 하나만 함수 템플릿으로 만들 수도 있다.

class Some
{
public:
	template <typename T> // 멤버 함수 하나만 함수 템플릿으로 만듬
	void memfunc(T a)
	{
		std::cout << a << std::endl;
	}
};

이 경우 실제 어떤 멤버 함수가 호출되는가에 따라 클래스 Some의 멤버 함수 개수가 결정된다.

 


2. 템플릿의 위치

클래스 템플릿 선언문은 반드시 사용하기 전에 와야 한다.

 

즉, 지금까지에서의 예제에서는 템플릿 선언문이 "main" 보다 먼저 선언되어 있기 때문에 컴파일이 된것이다. 이렇게 한번 전방 선언이 되면 템플릿 클래스의 멤버 함수 본체 정의문은 앞쪽에 이미 소속과 원형이 선언되어 있으므로 main 함수보다 뒤에 있어도 된다.

 

하지만, 예제에서는 한 파일에 템플릿 선언, 정의, 호출까지 작성했지만, 실제 프로젝트에서는 클래스별로 모듈을 구성하는 것이 일반적이다.

 

이때, 클래스 템플릿의 경우도 마찬가지로 별도의 모듈을 작성할 수 있지만 "템플릿 선언문과 멤버 함수의 정의까지 모두 헤더 파일에 작성해야 한다".

 

멤버 함수를 정의하는 함수 템플릿은 실제로 함수의 본체를 만드는 것이 아니므로 구현 파일에 작성해서는 안된다. 만약 "Value" 클래스 템플릿을 "Value.hpp"에 선언하고 이 클래스에 속한 멤버 함수에 대한 정의를 "Value.cpp"에 따로 정의한다고 해보자.

 

#include "value.hpp"

template <typename T>
Value<T>::Value(int ax, int ay, T av) : x(ax), y(ay), value(av)
{}

template <typename T>
void Value<T>::outValue()
{
	std::cout << x << ' ' << y << std::endl;
	std::cout << "value: " << value << std::endl;
}

 이때 outValue 함수는 Value.cpp 안에서만 알려지므로 다른 모듈에 있는 main 함수에서는 OutValue가 정의되지 않은 것으로 인식되어 에러로 처리한다.

 

일반 함수는 컴파일시에 원형만 선언되면 컴파일 가능하고 링크할 때 바인딩되는데 비해 템플릿은 컴파일할 때 완벽하게 구체화되어야 하므로 같은 번역 단위안에 선언이 있어야 한다.

 

하지만 헤더 파일에 클래스 템플릿을 두게 되면 최종 사용자에게 이 클래스의 코드를 숨길 수 없다는 단점이 있다. 이는 소스가 누출될 수 있는 보안상의 문제가 있는 것이다.

 

그래서 최신 표준 C++에서는 cpp 파일에 클래스 템플릿의 멤버 함수를 정의할 수 있는 export 키워드를 도입하고 이 키워드를 사용하면 구현 파일에 정의된 멤버 함수가 외부로도 알려지도록 한다.

export template <typename T>
void Value<T>::outValue() {...}

하지만 이는 몇몇 시험적인 컴파일러에서만 지원하고 대부분의 컴파일러에서는 지원하지 않는다.

 

그래서 대부분의 템플릿 라이브러리들은 거의 대부분 소스가 공개되어 있다.


3. 비타입 인수

지금까지 템플릿의 인수 목록에 전달되는 것은 통상 타입이었다.

 

하지만 타입이 아닌 상수템플릿 인수로 전달할 수 있는데 이를 비타입 인수(Nontype Argument)이라고 한다.

#include <iostream>

template <typename T, int N>
class Array
{
private:
	T arr[N];
public:
	void SetAt(int n, T v)
	{
		if (n < N && n >= 0)
			arr[n] = v;
	}
	T GetAt(int n)
	{
		return (n < N && n >= 0 ? arr[n] : 0);
	}
};

int main(void)
{
	Array<int, 5> ari;
	ari.SetAt(1, 1234);
	ari.SetAt(1000, 56798);
	std::cout << ari.GetAt(1) << std::endl;
	std::cout << ari.GetAt(5) << std::endl;
	return 0;
}

사실 임의 크기를 지원하는 가장 안전한 배열을 만드는 방법 중 하나는 생성자에 값을 넘겨 동적 배열을 만드는 것이다.

 

하지만 이렇게 할 경우 깊은 복사 등의 그에 따른 많은 코드를 만들어야 해서 코드의 부담감이 있다.

 

하지만 위처럼 하게 될 경우 부담도 적고 필요한 크기만큼의 요소를 정적으로 가지는 클래스를 만들어 쓰면 속도도 빠르고 위험하지도 않으며 무엇보다 단순하다. 이를 위한 (생성자, 파괴자, 복사 생성자, 대입 연산자) 중 어떤 것도 필요 없다.

 

물론, 각 완전히 같지 않은 각 인스턴스에 대해 개별적으로 메모리를 할당하는 부분에 대해서는 메모리 낭비가 있지만 말이다.

 

그리고 주의해야 할 점이 하나 있다.

int size = 5;
Array<int, size> ari; // 에러

const int size = 5;
Array<int, size> ari; // 가능

아래처럼 상수가 아닌 변수로는 할당할 수 없다.


5. 디폴트 템플릿 인수

함수의 디폴트 인수는 함수를 호출할 때 생략된 인수에 대해 기본적으로 적용되는 값이다.

 

클래스 템플릿에도 이와 비슷한 개념이 있는데 객체 선언문에서 인수를 생략할 경우 템플릿 선언문에서 지정한 디폴트가 적용된다.

 

template <typename T=int>
class Value
{...}

이렇게 선언하고 정수형의 객체를 선언할 때 다음처럼 객체를 생성할 수 있다.

Value<> iv(1, 2, 3);

"<>"는 생략하면 안된다.

 

만약 타입을 여러 개 가지는 클래스의 경우 오른쪽 인수부터 차례대로 디폴트를 지정할 수 있다. 그리고 객체를 서언할 때는 오른쪽부터 순서대로 생략 가능하다.

 

함수의 디폴트 인수와 같다.

 

하지만 함수 템플릿에는 디폴트를 정의할 수 없다. 왜냐하면 함수는 호출할 때 실인수의 타입을 보고 구체화할 함수를 결정하기 때문이다. 이것이 생략되면 컴파일러가 어떤 타입의 함수를 원하는지 알 방법이 없다.

 


6. 특수화

클래스 템플릿도 함수 템플릿과 마찬가지로 실제 클래스 타입이 사용될 때만 구체화한다.

 

만약 특정 타입에 대해 미리 클래스 선언을 만들어 놓을 필요가 있다면 명시적 구체화를 할 수 있다.

template class Value<float>;

이렇게 선언함으로써 컴파일러는 클래스 선언과 클래스 소속의 멤버 함수들을 모두 구체화해 둘 것이다.

 

또한, 특정 타입에 대한 클래스를 따로 생성하는 특수화도 물론 지원된다.

template <typename T>
class Value
{
private:
	int x, y;
	T value;
public:
	Value(int ax, int ay, T av);
	void outValue();
};

template <typename T>
Value<T>::Value(int ax, int ay, T av) : x(ax), y(ay), value(av)
{}

template <typename T>
void Value<T>::outValue()
{
	std::cout << x << ' ' << y << std::endl;
	std::cout << "value: " << value << std::endl;
}

template <> class Value<float>
{
private:
	int x, y;
	float value;
public:
	Value(int ax, int ay, float av) : x(ax), y(ay), value(av)
	{}
	void outValue()
	{
		std::cout << "나는 다르게 행동하지롱~" << std::endl;
	}
};

이렇게 함으로써 특수화를 진행할 수 있다.

 

그리고 외부에서 함수를 정의할 때 "template <>"를 붙이지 않아도 상관없다.

 

특수화를 하면 특수화된 클래스는 객체를 선언하지 않더라도 자동으로 구체화된다. 즉, 클래스 정의가 만들어지고 멤버 함수들은 컴파일되어 실행 파일에 포함된다.

 

따라서 일반적인 템플릿 클래스와는 달리 헤더 파일에 작성하면 안되며 구현 파일에 작성해야 한다.

 

또한 부분 특수화라는 것도 가능하다.

template <typename T1, typename T2> class Some {...}; // 예시 클래스

template <typename T1> class Some<T1, double> {...} // 부분 특수화 클래스

이 상태에서 Some<int, unsigned>나 Some<int, char>은 특수화되지 않는 버전의 템플릿으로부터 생성되지만, Some<int, double>이나 Some<char, double>은 부분 특수화된 템플릿으로부터 생성될 것이다.


출처

http://www.soen.kr/lecture/ccpp/cpplec.htm

 

C/C++ 강좌

 

www.soen.kr

 

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

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

관련글 더보기