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

꿈별·2023년 5월 31일
0

C++

목록 보기
20/27

✔함수 템플릿

: 함수를 찍어내는 틀

  • 아래와 같이 이름은 같고 인자 타입이 다른 함수 오버로딩을 하는 경우
int Add(int a, int b)
{
	return a + b;
}

float Add(float f, float f1)
{
	return f + f1;
}

-> 두 인자를 더한 값을 반환하는 기능은 동일한데, 인자 타입이 달라질 때마다 새로 정의해야 해서 불편하다..
👉 💡 함수 템플릿으로 해결


문법

template <typename 타입이름>
함수의 원형
{
	//함수의 본체
}

  • 위의 Add 함수를 함수 템플릿으로 만들어 보자
template<typename T>
T Add(T a, T b)
{
	return a + b;
}

타입이름 T : 치환 자료형. T가 들어가는 부분에 원하는 걸 넣을 수 있음.

  • Add 함수 호출하기
int i = Add<int>(10, 20);

-> 함수 템플릿의 T 위치에 int 가 들어간 함수 요청함

  • 함수 템플릿을 선언해 놓고, 한 번도 호출하지 않는다면 Add 함수는 코드상에 존재하는 것일까?
    -> 함수 템플릿은 typename 대신 어떤 타입이 들어갈지 요청할 때 함수가 만들어진다. 따라서 함수 틀만 만들어 놓고 찍어내지 않는다면 함수는 존재하지 않는 것 이다.

  • 컴파일러가 적절하게 인식하는 경우

int i = Add(10, 20);

-> typename 을 생략해도, 입력으로 넣어주는 두 데이터가 정수이고 전달받는 변수 i가 정수형이기 때문에 int 타입으로 요청한 것으로 받아들인다.
-> ❗ Add(10, 20)이 함수 호출하는 것으로 보이지만 함수가 아니라 함수 템플릿이다.

✔클래스 템플릿

: 클래스를 찍어내는 틀

사용하는 이유

  • 기본 문법 데이터 타입이 아니라, 내가 만든 구조체를 데이터로 저장하는 가변 배열 클래스를 만들고 싶다면?
    -> 데이터 타입별로 클래스를 만들어야 한다. 번거롭다 ..
    👉 💡 클래스 템플릿으로 해결

문법

template <typename 타입이름>
class 클래스템플릿이름
{
    // 클래스 멤버의 선언
}

(1) 클래스 템플릿 예제

: 가변배열 구현한 클래스 CArr를 클래스 템플릿으로 만들어 보자

class CArr
{
private:
	int*	m_pInt;
	int		m_iCount;
	int		m_iMaxCount;

public:
	void push_back(int _Data);
	void resize(int _iResizeCount);
	int& operator[] (int idx);

public:
	CArr();
	~CArr();
};

template<typename T>
class CArr
{
private:
	T*		m_pData;
	int		m_iCount;
	int		m_iMaxCount;

public:
	void push_back(const T& _Data);
	void resize(int _iResizeCount);
	T& operator[] (int idx);

public:
	CArr();
	~CArr();
};
  • T* m_pData;
    int m_iCount;
    int m_iMaxCount;
    -> 클래스 멤버변수 타입을 전부 타입이름으로 바꾸는 게 아니다. 상황에 따라 적절히 바꿔야 한다.
    -> 가변배열의 한계치와 최대치 의미하는 m_iCountm_iMaxCountint로 표현해야 하는 게 맞음.
    -> 어떤 타입의 데이터든 받아올 수 있어야 하므로, 데이터값 나타내는 포인터 변수 m_pInt의 이름을 m_pData로 바꾸고, 타입도 int* 에서 T* 로 바꾼다.

  • void push_back(const T& _Data);
    : 함수 설계 원리를 먼저 생각해 본다.

    원래 가변 배열에 데이터 추가할 떄는, 추가할 데이터 값을 함수에 복사시킨 뒤, 함수가 가리키는 힙 메모리 공간에 집어넣었다.
    But 클래스 템플릿을 활용한 가변 배열은 어떤 타입의 데이터든 추가할 수 있어야 한다.
    따라서 사용자가 지정한 타입을 저장할 수 있게 되는데, 이러면 저장하는 데이터 단위가 얼마나 커질지 알 수 없다.
    데이터 단위가 너무 커지면, 함수에 데이터값을 지역변수로 복사하고 힙 메모리에 넣는 기존 방식은 비용이 커지고 비효율적이게 된다.

👉 원본은 수정하지 않되, 참조만 받아올 수 있도록 const 레퍼런스 형태로 데이터를 받아온다.

  • T& operator[] (int idx);
    : void push_back(const T& _Data); 에서 참조하는(추가되는) 데이터가 T 타입으로 저장되어 있으므로 int& -> T& 로 바꿔줘야 한다.

❗ 주의점
-> 템플릿의 함수는 선언/정의를 헤더/cpp 파일로 나누지 말고,
헤더 파일에 한꺼번에 작성해야 한다.
Why?
컴파일러는 작성된 코드를 파일 단위로 컴파일한 뒤 링크 과정을 거쳐 이를 합친다.
근데 만약 클래스 템플릿의 함수 정의가 cpp 파일에 있다면 어떻게 될까?

main.cpp 에서 내가 만든 클래스 템플릿 헤더를 참조한다고 가정한다.
메인에서 템플릿에 전달되는 인수 타입이 float 이라는 코드를 작성하면,
컴파일러가 메인으로 가서 이 요청을 확인한 다음
헤더 파일로 가서, typename T로 선언된 부분을 float 으로 바꾼다.
근데 헤더 파일에 선언만 있고 실제 구현이 없다.
-> 따라서 선언의 T만 바뀐거지 실제 구현 코드의 Tfloat으로 바뀌지 않았다. (아직 함수가 만들어지지 않았음)
만약 나중에 실제 구현부분이 있는 cpp 파일을 컴파일한다고 했을 때는,
타입네임 T로 되어있는 템플릿 원형만 있고, 저 T를 어떤 자료형으로 쓸지는 모른다.

👉 선언과 정의를 같은 곳(헤더)에서 해야 헤더를 참조하는 쪽이 요청하는 템플릿의 인수 타입을 파악할 수 있다.
(클래스에서는 cpp 파일의 함수들이 즉시 사용될 것이므로 바로 컴파일된다.
But 템플릿은 즉시 사용할 수 없고, 타입을 정해줘야 함수가 만들어지게 되므로 그 전까지는 컴파일할 수 없다.


(2) 클래스 외부의 멤버 함수 템플릿

CArr::CArr()
	: m_pData(nullptr)
	, m_iCount(0)
	, m_iMaxCount(2)
{
	//int 자료형 2개 크기만큼 동적할당하겠다는 의미
	m_pData = new int[2];
}

CArr::~CArr()
{
	delete[] m_pData;
}

void CArr::push_back(int _Data)
{
	//힙 영역에 할당한 공간이 다 찬 경우
	if (m_iMaxCount <= m_iCount)
	{
		//재할당
		resize(m_iMaxCount * 2);
	}
	//데이터 추가한 뒤, 후위 연산자로 한계치 1 증가
	m_pData[m_iCount++] = _Data;
}

void CArr::resize(int _iResizeCount)
{
	// 현재 최대 수용량 보다 더 적은 수치로 확장하려는 경우
	if (m_iMaxCount >= _iResizeCount)
	{
		assert(nullptr);
	}

	// 1. 리사이즈 시킬 개수만큼 동적할당하기
	int* pNew = new int[_iResizeCount];

	// 2. 기존 공간 -> 새로운 공간 으로 데이터 복사
	for (int i = 0; i < m_iCount; ++i)
	{
		pNew[i] = m_pData[i];
	}
	// 3. 기존 공간은 메모리 해제
	delete[] m_pData;

	// 4. 배열이 새로 할당된 공간을 가리키게 한다.
	m_pData = pNew;

	// 5. MaxCount 변경점 적용
	m_iMaxCount = _iResizeCount;
}

int& CArr::operator[](int idx)
{
	return m_pData[idx];
}

template<typename T>
CArr<T>::CArr()
	: m_pData(nullptr)
	, m_iCount(0)
	, m_iMaxCount(2)
{
	//int 자료형 2개 크기만큼 동적할당하겠다는 의미
	m_pData = new T[2];
}

template<typename T>
CArr<T>::~CArr()
{
	delete[] m_pData;
}

template<typename T>
void CArr<T>::push_back(const T& _Data)
{
	//힙 영역에 할당한 공간이 다 찬 경우
	if (m_iMaxCount <= m_iCount)
	{
		//재할당
		resize(m_iMaxCount * 2);
	}
	//데이터 추가한 뒤, 후위 연산자로 한계치 1 증가
	m_pData[m_iCount++] = _Data;
}

template<typename T>
void CArr<T>::resize(int _iResizeCount)
{
	// 현재 최대 수용량 보다 더 적은 수치로 확장하려는 경우
	if (m_iMaxCount >= _iResizeCount)
	{
		assert(nullptr);
	}

	// 1. 리사이즈 시킬 개수만큼 동적할당하기
	T* pNew = new T[_iResizeCount];

	// 2. 기존 공간 -> 새로운 공간 으로 데이터 복사
	for (int i = 0; i < m_iCount; ++i)
	{
		pNew[i] = m_pData[i];
	}
	// 3. 기존 공간은 메모리 해제
	delete[] m_pData;

	// 4. 배열이 새로 할당된 공간을 가리키게 한다.
	m_pData = pNew;

	// 5. MaxCount 변경점 적용
	m_iMaxCount = _iResizeCount;
}

template<typename T>
T& CArr<T>::operator[](int idx)
{
	return m_pData[idx];
}

-> CArr<T> 까지 써야 클래스 의미함, CArr 만 쓰면 템플릿


- main에서 float 저장하는 가변배열 클래스 요청 예시
CArr<float> carr;
carr.push_back(3.14f);
carr.push_back(6.28f);
carr.push_back(30.444f);

float fData = carr[1];

[참고]
https://youtu.be/LyxeYRNXM0E
http://www.tcpschool.com/cpp/cpp_template_function
http://www.tcpschool.com/cpp/cpp_template_class
https://learn.microsoft.com/ko-kr/cpp/cpp/member-function-templates?view=msvc-170

0개의 댓글