본문 바로가기
C++

[C++]함수 템플릿, 클래스 템플릿

by Junk_Seo 2018. 3. 5.
반응형

템플릿

템플릿(template)은 C++ 프로그래밍 언어의 한 기능으로, 함수와 클래스가 제네릭 형과 동작할 수 있게 도와준다. 함수나 클래스가 개별적으로 다시 작성하지 않고도 각기 다른 수많은 자료형에서 동작할 수 있게 한다. 이는 튜링 완전 언어로 볼 수 있다.

템플릿은 C++에서 프로그래머들에게 유용한데, 특히 다중 상속과 연산자 오버로딩과 결합할 때 그러하다. C++ 표준 라이브러리는 연결된 템플릿의 프레임워크 안에서 수많은 유용한 함수들을 제공한다.

(제네릭 형 : 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식이다.)

출처 : https://ko.wikipedia.org/wiki/%ED%85%9C%ED%94%8C%EB%A6%BF_(C%2B%2B)

 

템플릿은 자료형에 상관없이 범용적으로 사용할 수 있도록 하는 방법입니다. 하지만 일반적으로 명시적으로 코드를 작성하여 사용자가 

알아보기 쉽게 코딩하는 것이 좋기 때문에 템플릿을 직접 구현하여 사용할 일은 없을 겁니다.(아마?)

프로그래머와 사용자의 커뮤니케이션이 명확해야 하는데, 템플릿을 사용할 경우 이 커뮤니케이션이 깨지기 때문입니다.

그래도 C++ 자료구조(STL)의 경우 템플릿으로 구현되어 있기 때문에 이를 사용하기 위해서는 템플릿에 대해서 알고 있어야 합니다.

템플릿 동작원리

프로그램 코드는 컴파일과 링크과정을 거치는데, 템플릿은 컴파일을 하는 과정에서 결정됩니다.

 

컴파일 : 문법 오류 검사, 메모리에 어떤 방식으로 올릴지 설계한다.

링커 : 함수 or 클래스의 선언과 구현을 연결시켜 주는 일을 한다.

 

보통 컴파일 시에 컴파일러는 코드를 순차적으로 읽으면서 진행합니다. 함수의 선언부를 읽어서 함수가 존재함을 인지하고(함수에서 사용되는 인자들의 자료형 등)  함수를 사용하는 곳(main 이겠죠?)에서 함수를 확인합니다. 

그리고 함수 구현부를 확인하여 메모리에 어떻게 올릴지를 결정합니다. 

그리고 링크 과정에서 함수의 선언부와 구현부를 연결시킵니다. 

 

하지만 템플릿의 경우 초기에는 자료형이 정해져 있지 않기 때문에 컴파일 과정에서 템플릿을 일단 무시하고 지나갑니다.

그리고 컴파일을 하다가 템플릿 함수와 같은 함수를 호출하는 시점을 찾으면 일단 자료형에 맞는 일반 함수를 찾고 없다면 템플릿 함수를 참조합니다.

그리고 그 형식이 같다면 그 템플릿 함수를 호출한 함수의 자료형인 함수를 만듭니다. 그리고 템플릿 함수의 구현부를 사용하는 자료형의 형식으로 만듭니다.

(컴파일 시에는 반환형을 신경 쓰지 않기 때문에 함수의 인자를 기준으로 확인합니다.)

함수 템플릿

****

함수 템플릿 : 코드로 보이는 템플릿으로 함수를 기반으로 구현이 된 템플릿을 함수 템플릿이라고 합니다.

템플릿 함수 : 템플릿을 기반으로 컴파일러가 만들어낸 함수를 템플릿 함수라고 합니다.

 

함수의 형식

리턴 [옵션] 이름<템플릿>(인수, 인수...);

template <typename T>
T add(T tData1, T tData2);

int main()
{
    printf("%d \n", add(10, 20));
//  printf("%d \n", add<int>(10, 20));
    return 0;
}

template<typename T>
T add<T>(T tData1, T tData2)
{
	return tData1 + tData2;
}

이 코드에서 템플릿으로 만들어진 함수 템플릿 add가 있는데, 위에서 말했듯이 컴파일 시에 함수 템플릿의 자료형을 알지 못하기 때문에 그냥 지나친 다음  add 함수를 호출하는 시점에서 일단 일반 함수가 있는지 찾고 없다면 함수 템플릿인 add를 자료형이 int형인 add 템플릿 함수를 만들어서 설계를 합니다.

그리고 함수 템플릿인 add의 구현부로 넘어가면 이 함수를 int형 add 템플릿 함수로 따로 만들어줍니다. 

만약 다른 자료형이 또 나온다면 같은 방식으로 진행하면서 자료형만 다른 템플릿 함수를 또 만듭니다.

즉, int형 add, float형 add 함수 2개가 있다면 자료형이 int인 add 템플릿 함수와 float 템플릿 함수 2개를 따로 만듭니다.

내가 int형, double형, float형 add 함수를 호출한다면 이 3가지 함수를 만들어서 사용하는 것과 같습니다. 

 

템플릿 함수를 사용할 때 함수 뒤에 있는 템플릿<> 을 생략하는 것이 가능합니다. 함수의 이름 뒤에 <>템플릿을 생략하면 먼저 일반 함수를 찾고, 없으면 템플릿 함수를 찾습니다.

하지만 주석을 친 코드처럼 add<int>(10, 20)으로 사용하면 컴파일 시에 일반함수는 찾지 않고 바로 템플릿 함수를 찾습니다.

 

템플릿을 사용하면 겉으로 보기에는 하나의 함수를 범용적으로 사용하는 것처럼 보이지만 내부적으로 구조를 보면 사용하는 자료형에 따라 계속해서 함수를 만들어서 사용합니다.

따라서 함수 오버로딩을 사용하는 것과 별 차이가 없습니다. 

그래서 템플릿을 사용하는 것보다 필요한 자료형에 따라 함수를 오버로딩하는 것이 명시성이 높아져 사용자가 사용하기에 좋습니다.

함수 템플릿과 일반 함수

template <typename T>
T add(T tData1, T tData2);

int add(int nData1, int nData2);

int main()
{
	printf("%d \n", add<int>(10, 20));
	printf("%d \n", add(10, 20));
	return 0;
}

template<typename T>
T add<T>(T tData1, T tData2)
{
	printf("템플릿 함수 \n");
	return tData1 + tData2;
}

int add(int nData1, int nData2)
{
	printf("일반 함수 \n");
	return nData1 + nData2;
}

위 코드에서 add<int> 함수는 함수 템플릿을 자료형이 int인 템플릿 함수를 만들어서 사용하고, 그냥 add 함수는 일반 add 함수를 사용합니다.

템플릿으로 된 add 템플릿 함수와 int형으로 된 일반 add 함수 2개가 있습니다. 이때 add 함수를 사용하는데 add<int>(10, 20)처럼 템플릿을 명시하면 템플릿 함수를 호출하고, 

add(10, 20)처럼 템플릿을 생략하여 사용하면 일반 함수를 호출합니다.

이처럼 템플릿 함수와 일반함수가 동시에 존재한다면 일반함수가 우선순위가 높습니다.

따라서 템플릿 함수를 사용할 거라면 함수의 이름 뒤에 <>템플릿을 생략하지 말고 사용해야 합니다.

(C++에서는 생략할 수 있다고 해서 생략하면 딱히 얻는 이점이 없으므로 될 수 있으면 생략하지 않고 사용합시다.)

 

****

템플릿 함수의 선언 부에서는 함수의 이름 뒤에는 템플릿이 생략되어 있고, 구현부에는 T add<T>(T tData1, T tData2)처럼 템플릿이 함수 이름 뒤에 있는데, 이유는 선언부에 템플릿을 넣으면 error가 발생합니다.(이유는 모르겠네요) 물론 구현부에서도 저 템플릿을 생략하여 T add(T tData1, T tData2){...} 작성해도 되지만 생략하지 말고 사용합시다. 

함수 템플릿 예외 처리

자료형에 따라 템플릿에 예외를 처리해야 하는 경우가 있습니다.

template <typename T>
T add(T tData1, T tData2);

template<>
char add<char>(char cData1, char cData2);

//char add(char cData1, char cData2);


int main()
{
    printf("%d \n", add<int>(10, 20));
    add<char>('A', 'B');
    return 0;
}

template <typename T>
T add<T>(T tData1, T tData2)
{
    return tData1 + tData2;
}

template<>
char add<char>(char cData1, char cData2)
{
    printf("%c, %c \n", cData1, cData2);
    return '\0';
}

/*char add(char cData1, char cData2)
{
    printf("%c, %c \n", cData1, cData2);
    return '\0';
}*/

위 코드를 보면 add 함수 템플릿에서 char형 자료형은 문자이기 때문에 연산이 불가능합니다. 따라서 char 자료형의 경우 예외가 발생합니다.

그래서 char형 자료형은 따로 구현하여 템플릿으로 char 자료형을 사용하면 다른 함수를 부르도록 합니다. 

이 경우에도 char형 자료형의 함수를 부를 때 함수이름 뒤에 템플릿을 생략하면 주석 친 일반 함수가 있다면 일반 함수를 호출합니다.

 

템플릿은 범용적으로 사용하기 위한 것인데 예외처리가 있다는 것 자체가 설계에 문제가 있다는 뜻입니다. 

따라서 예외가 발생하면 템플릿을 사용하지 않는 것이 좋습니다.

템플릿 함수 파일 분할

***링크 error는 함수를 선언하고 구현하지 않고 사용하는 경우 발생***

 

함수 템플릿은 정의와 구현이 분리되어 있으면 링크 error가 발생한다.

즉, header파일에 함수 템플릿을 선언하고 cpp 파일에 함수 템플릿의 구현을 작성하면 링크 error가 발생하다.

따라서 header파일에 함수 템플릿을 선언했다면 그 header파일에 구현부도 작성해 주어야 한다.

(함수 템플릿은 선언과 구현부가 같은 파일에 존재해야 한다.)

어차피 선언과 구현이 같은 곳(같은 페이지)에 존재하기 때문에 선언과 구현을 합쳐서 사용해도 된다.

template<typename T>
T add(T t1, T t2);

template<>
char add<char>(char t1, char t2);

template<typename T>
inline T add<T>(T t1, T t2)
{
	return t1 + t2;
}

//template<>
//inline char add<char>(char t1, char t2)
//{
//	printf("%c %c \n", t1, t2);
//	return '\0';
//}

[add.cpp]
template<>
char add<char>(char t1, char t2)
{
	printf("%c %c \n", t1, t2);
	return '\0';
}

템플릿 함수는 함수 사용부에서 자료형을 보고 함수를 만들어서 메모리에 올릴 준비를 하는데, 템플릿 함수의 구현부가 정의부 페이지와 다른 곳에 있으면 

컴파일러가 템플릿 함수의 구현부를 찾지 못해서 함수를 만들지 못하고, 링커 과정에서 선언은 있는데 구현부가 없기 때문에 링크를 하지 못해서 error가 발생한다.

 

템플릿 예외 상황의 경우 예외 함수는 cpp에 구현한다. 

(그냥 header에 구현을 추가하면 정의 중복 error발생, 이때에는 inline 키워드를 붙여 줘야 한다. - 위 코드의 주석 부분)

클래스 템플릿

****

클래스 템플릿 : 코드로 보이는 템플릿으로 클래스를 기반으로 구현이 된 템플릿을 클래스 템플릿이라고 합니다.

템플릿 클래스 : 템플릿을 기반으로 컴파일러가 만들어낸 클래스를 템플릿 클래스라고 한다.

 

클래스에서는 템플릿이 네임스페이스 뒤에 들어간다.

template<typename T>
class C_DATA
{
private:
	T m_tData;

public:
	C_DATA();
	void setData(T tData);
	T getData();

};

int main()
{
	C_DATA<float> cData;
	cData.setData(10.10f);
	printf("%f \n", cData.getData());
    return 0;
}

template<typename T>
C_DATA<T>::C_DATA() :
	m_tData()
{
}

template<typename T>
void C_DATA<T>::setData(T tData)
{
	m_tData = tData;
}

template<typename T>
T C_DATA<T>::getData()
{
	return m_tData;
}

위 코드에서 멤버 함수 구현부를 확인하면 "C_DATA <T>::..."처럼 네임스페이스 뒤에 템플릿이 들어간다는 것을 알 수 있다.

그리고 코드에서 보면 

 

template<typename T>

class C_DATA

{...}

 

이렇게 클래스를 만드는데, 원래는 "template<typename T> class C_DATA" 이게 클래스 템플릿의 풀 네임이다. 

그래서 멤버 함수를 따로 구현할 때 template<typename T> 이 다 붙어있는 이유이다.

 

그리고 템플릿 클래스를 사용하기 위해서는 클래스 변수를 생성할 때 어떤 자료형을 사용할지를 알려줘야 한다. 

즉, 템플릿을 지정해주어야 한다. 

"C_DATA<int> cData;"처럼 해야 한다.

 

그리고 클래스 템플릿의 경우 멤버 함수의 구현을 따로 하는 것이 별의미가 없다.(header, cpp를 나눌 수 업식 때문)

어차피 header 파일에 다 구현하끼 때문에 다음과 같이 선언과 구현을 합쳐서 사용한다.

template<typename T>
class C_DATA
{
private:
	T m_tData;
public:
	C_DATA() : 
	m_tData()
	{
	}
	void setData(T tData) {
		m_tData = tData;
	}
	T getData() {
		return m_tData;
	}
};

클래스 템플릿 예외

[data.h]
template<typename T>
class C_DATA
{
private:
	T m_tData;
public:
	C_DATA() : 
	m_tData()
	{
		printf("클래스 템플릿 \n");
	}
	void setData(T tData) {
		printf("클래스 템플릿 \n");
		m_tData = tData;
	}
	T getData() {
		printf("클래스 템플릿 \n");
		return m_tData;
	}
};

template<>
class C_DATA<char>
{
private:
	char m_cData;
public:
	C_DATA();
	void setData(char cData);
	char getData();
};

[data.cpp]
C_DATA<char>::C_DATA() :
	m_cData(0)
{
	printf("클래스 템플릿 예외\n");
}

void C_DATA<char>::setData(char cData)
{
	printf("클래스 템플릿 예외\n");
	m_cData = cData;
}

char C_DATA<char>::getData()
{
	printf("클래스 템플릿 예외\n");
	return m_cData;
}

클래스 템플릿도 함수 템플릿처럼 예외가 발생하는 자료형에 대해서는 따로 예외를 구현해주어야 한다.

클래스 템플릿 예외는 클래스를 정의할 때 

template<>

class C_DATA<char>

처럼 클래스 이름 뒤에 예외 자료형을 템플릿으로 명시해주어야 합니다.

그리고 클래스 템플릿과 똑같이 멤버와 함수를 선언하는데, 이때에는 예외 자료형에 맞춰서 코딩을 합니다.

그리고 구현은 cpp 파일에 합니다. (inline으로 선언하면 같은 header에 구현이 가능합니다.)

 

그리고 멤버 함수 구현부에 templete<>를 붙여줘야 할 것 같지만 붙이면 error가 발생하므로 빼주어야 합니다.

 

 

반응형