C++/C++ 기초

[C++] 제네릭 클래스 ( Generic class )

Song 컴퓨터공학 2023. 7. 17. 20:18
 

[C++] 템플릿 ( Template ) - 함수 템플릿 / 클래스 템플릿

C++ 이 가지는 특징 중 하나로 일반화 프로그래밍(Generic programming) 을 들 수 있습니다. 일반화 프로그래밍이란 쉽게 말하자면 일반적인, 다양한 상황에서도 같은 코드로 적용할 수 있는 것을 말합

songsite123.tistory.com

위 포스팅에서 제너릭의 개념과 이를 위한 template 의 사용법과 예제를 확인했습니다. 맨 마지막에 제네릭 클래스에 대해 다루긴 했는데요 너무 간단하게 다룬 것 같아 오늘은 예제 위주로 확인해보며 template 를 이용한 제네릭 클래스에 대해 더 자세히 알아보는 포스팅입니다.

 

 

제네릭 클래스를 만들기 위해서는, 클래스의 선언부와 구현부를 모두 template 로 선언해야 합니다. 제네릭을 이용한 스택 클래스 코드를 보면서 자세히 확인해보도록 하겠습니다.

class Point {
	int x, y;
public:
	Point(int x = 0, int y = 0) { this->x = x; this->y = y; }
	void show() { cout << '(' << x << ',' << y << ')' << endl; }
};

template <class T>
class MyStack {
	int tos;
	T data[100];
public:
	MyStack();
	void push(T element);
	T pop();
};

template <class T>
MyStack<T>::MyStack() { tos = -1; }

template <class T>
void MyStack<T>::push(T element) {
	if (tos == 99) { 
		cout << "stack full";
		return;
	}
	data[++tos] = element;
}

template <class T>
T MyStack<T>::pop() {
	T retData;
	if (tos == -1) {
		cout << "stack empty";
		return 0;
	}
	retData = data[tos--];
	return retData;
}

먼저 기본 클래스인 Point 클래스와 이를 상속 받은 MyStack 클래스가 있습니다. 스택 클래스는 data의 타입을 정해놓지 않고 제너릭 타입 T를 사용하기 때문에 선언 위에 template <class T> 를 사용합니다. 그리고 push 와 pop 또한 원형만 선언했는데 이 두 멤버 함수도 제너릭 타입을 사용합니다.

 

여기서 빼먹기가 쉬운데, 클래스 외부에서 멤버 함수를 구현할 때에도 제너릭 타입을 쓰는 경우 template <class T> 를 사용해야 한다는 점입니다. 이 때문에 선언부와 구현부 모두 template 선언 이라고 위에서 말한 겁니다.

 

이렇게 제너릭 타입으로 선언한 클래스를 구체화 하는 방법은 클래스이름<자료형> 입니다. 제네릭 클래스를 이용할 때는 클래스의 이름과 함께 제네릭 타입에 적용할 구체적인 타입을 필수적으로 지정해줘야 합니다.

int main() {
	MyStack<int*> ipStack;
	int* p = new int[3];
	for (int i = 0; i < 3; i++) p[i] = i * 10;
	ipStack.push(p);
	int* q = ipStack.pop();
	for (int i = 0; i < 3; i++) cout << q[i] << ' ';
	cout << endl;
	delete[] p;

	MyStack<Point> pointStack;
	Point a(2, 3), b;
	pointStack.push(a);
	b = pointStack.pop();
	b.show();

	MyStack<Point*> pStack;
	pStack.push(new Point(10, 20));
	Point* pPoint = pStack.pop();
	pPoint->show();

	MyStack<string> stringStack;
	string s = "c++";
	stringStack.push(s);
	stringStack.push("java");
	cout << stringStack.pop() << ' ';
	cout << stringStack.pop() << endl;
}

다양한 예에 대해 MyStack 클래스의 객체를 생성해 사용하는 코드입니다. 보면 Point 타입 객체로도 선언하고, int*, string , Point* 등 다양한 자료형에 대해 스택 클래스를 사용하고 있습니다. 아래는 전체 코드 및 실행 결과입니다.

#include <iostream>
#include <string>
using namespace std;
class Point {
	int x, y;
public:
	Point(int x = 0, int y = 0) { this->x = x; this->y = y; }
	void show() { cout << '(' << x << ',' << y << ')' << endl; }
};

template <class T>
class MyStack {
	int tos;
	T data[100];
public:
	MyStack();
	void push(T element);
	T pop();
};

template <class T>
MyStack<T>::MyStack() { tos = -1; }

template <class T>
void MyStack<T>::push(T element) {
	if (tos == 99) { 
		cout << "stack full";
		return;
	}
	data[++tos] = element;
}

template <class T>
T MyStack<T>::pop() {
	T retData;
	if (tos == -1) {
		cout << "stack empty";
		return 0;
	}
	retData = data[tos--];
	return retData;
}

int main() {
	MyStack<int*> ipStack;
	int* p = new int[3];
	for (int i = 0; i < 3; i++) p[i] = i * 10;
	ipStack.push(p);
	int* q = ipStack.pop();
	for (int i = 0; i < 3; i++) cout << q[i] << ' ';
	cout << endl;
	delete[] p;

	MyStack<Point> pointStack;
	Point a(2, 3), b;
	pointStack.push(a);
	b = pointStack.pop();
	b.show();

	MyStack<Point*> pStack;
	pStack.push(new Point(10, 20));
	Point* pPoint = pStack.pop();
	pPoint->show();

	MyStack<string> stringStack;
	string s = "c++";
	stringStack.push(s);
	stringStack.push("java");
	cout << stringStack.pop() << ' ';
	cout << stringStack.pop() << endl;
}

 

 

2개 이상의 제너릭 타입을 가지는 경우는 template <class T1, class T2, ... > 와 같은 방식으로 여러 개의 제너릭 타입을 선언하여 사용할 수 있습니다. 

// 두 개의 제네릭 타입 사용하는 제네릭 클래스
#include <iostream>
using namespace std;

template <class T1, class T2>
class GClass {
	T1 data1;
	T2 data2;
public:
	GClass();
	void set(T1 a, T2 b);
	void get(T1& A, T2& B);
};

template <class T1, class T2>
GClass<T1, T2>::GClass() {
	data1 = 0;
	data2 = 0;
}

template <class T1, class T2>
void GClass<T1, T2>::set(T1 a, T2 b) {
	data1 = a;
	data2 = b;
}

template <class T1, class T2>
void GClass<T1, T2>::get(T1& A, T2& B) {
	A = data1;
	B = data2;
}

int main() {
	int a;
	double b;
	GClass <int, double> x;
	x.set(2, 0.5);
	x.get(a, b);
	cout << "a= " << a << "\t" << "b= " << b << endl;

	char c;
	float d;
	GClass <char, float> y;
	y.set('s', 4.2);
	y.get(c, d);
	cout << "c= " << c << "\t" << "d= " << d << endl;
}

 

 

마지막으로 연산자 오버로딩 및 friend 로 멤버를 선언할 때 제너릭 타입을 적용하는 예시입니다.

#include <iostream>
using namespace std;

template <typename T>
class Point{
	T x, y;
public:
	Point(T a = 0, T b = 0) :x(a), y(b) { }
	template <typename T>
	friend ostream& operator<<(ostream& out, Point<T>& Po);
};

template <typename T>
ostream& operator<<(ostream& out, Point<T>& Po)
{
	out << "(" << Po.x << ", " << Po.y << ")" << endl;
	return out;
}

int main() {
	Point<int> a(3, 5);
	cout << a << endl;
	Point<double> b(3.4, 5.5);
	cout << b << endl;
}

중간에 아주 중요한게 friend 키워드를 사용할 때 만약 제너릭 타입을 매개변수로 사용한다면, 외부 구현부 뿐만 아니라 선언부 내부에도 추가적으로 template <typename T> 를 써줘야만 합니다. 저 한 줄이 없다면 컴파일 오류가 발생합니다.

 

 

즉 제너릭 클래스의 경우 구현부와 선언부 모두에 template <class 이름> 이나 template <typename 이름> 을 사용해주어야 컴파일러가 제대로 인식합니다. 제네릭 클래스의 경우 헷갈리기도 하고, 또 연산자 오버로딩이나 프렌드의 경우에도 섞이면 많이 헷갈리기 때문에 구현부와 선언부 모두 라는 것을 잘 기억하시면 될 것 같습니다. 감사합니다.