💻 개인공부 💻/C | C++

[C++] 제네릭 함수 (feat. template)

공대생 배기웅 2020. 6. 21. 03:41
반응형

함수 중복의 약점과 제네릭

 

template <class T> 
//template: 템플릿을 선언하는 키워드 
//class: 제네릭 타입을 선언하는 키워드 
//T: 제네릭 타입 T 선언 
void swap(T& a, T& b) { 
	T tmp; 
	tmp a; 
	a = b; 
	b = tmp; 
}

 

▶ 위의 소스 코드를 보자

 

template <class T> 
//template: 템플릿을 선언하는 키워드 
//class: 제네릭 타입을 선언하는 키워드 
//T: 제네릭 타입 T 선언 
void swap(T& a, T& b) { 
	T tmp; 
	tmp a; 
	a = b; 
	b = tmp; 
}

 

둘다 동일한 코드에다가 동일한 이름이지만, 단순히 매개 변수만 달라(오버로딩) 중복하여 작성하였다. 이는 코드를 작성하는데 낭비가 될 수 있다. 

이를 해결하기 위해 사용하는 것이 제네릭(generic, 일반화)이다. 제네릭이란 함수나 클래스를 일반화시키고, 매개 변수 타입을 직접 지정하여 틀에서 직어 내듯이 함수나 클래스 코드를 생산하는 기법이다. 

 

▶템플릿은 함수나 클래스를 일반화하는데 사용되는 C++ 도구이다. template 키워드로 함수나 클래스를 선언할 수 있다. 다음은 템플릿을 이용한 제네릭 함수 swap이다. 

 

template <class T> 
//template: 템플릿을 선언하는 키워드 
//class: 제네릭 타입을 선언하는 키워드 
//T: 제네릭 타입 T 선언 
void swap(T& a, T& b) { 
	T tmp; 
	tmp a; 
	a = b;
	b = tmp; 
}

 

 

제네릭 함수

#include<iostream>
using namespace std;

class Circle {
	int radius;//private
public:
	Circle(int radius = 1) {
		this->radius = radius;
	}
	int getRadius() {
		return radius;
	}
};

template<class T>
void myswap(T& a, T& b) {
	T tmp;
	tmp = a;
	a = b;
	b = tmp;
}

void main() {
	int a = 4, b = 5;
	myswap(a, b);
	cout << "a=" << a << "," << "b=" << b << endl;

	double c = 4, d = 5;
	myswap(c, d);
	cout << "a=" << a << "," << "b=" << b << endl;
	
	Circle donut(5), pizza(20);
	myswap(donut, pizza);
	cout << "donut 반지름=" 
    << donut.getRadius() << endl;
	cout << "pizza반지름=" 
    << pizza.getRadius() << endl;
}

▶ int 형의 수, double 형의 수, Circle 클래스의 객체 , 이 세 가지의 다른 형식으로 myswap( )메소드에 대입했음에도 결과가 오류없이 나온다. 

▶ 이처럼 템플릿을 사용하게 되면 함수의 재사용으로 인해 높은 sw의 생산성과 유용성에 기여한다. 하지만 포팅에 취약하고 컴파일 오류 메시지에 빈약하기에 디버깅에 많은 어려움을 겪는다고 한다.

 

▶ 다음은 제네릭 함수를 이용한 예제들이다. 

 

 

1. 큰 값을 리턴하는 bigger()함수

#include<iostream>
using namespace std;

template <class T>
T bigger(T a, T b) {
	if (a > b)
		return a;
	else return b;
}

void main() {
	int a = 20, b = 50;
	char c = 'a', d = 'z';
	cout << "bigger(20,50)의 결과는" 
    << bigger(a, b) << endl;
	cout << "bigger(a,z)의 결과는" 
    << bigger(c, d) << endl;
}

▶ class T로 지정을 하고, T는 int 형으로 쓰이기도 하고, char형으로도 쓰이기도 한다. 이를 통해 다양한 매개변수에서 쓰일 수 있음을 다시 한 번 강조한다.

 

2. 배열의 합을 구하여 return하는 제네릭 add( ) 메소드 예제 (feat. 배열과 변수가 매개변수인 template)

#include<iostream>
using namespace std;

template <class T>
T add(T data[], int n) {
	T sum = 0;
	for (int i = 0; i < n; i++) {
		sum += data[i];
	}
	return sum;
}

void main() {
	int x[] = { 1,2,3,4,5 };
	double d[] = { 1.1,2.2,3.3,4.4,5.5,6.6 };

	cout << "sum of x[] =" 
    << add(x, 5) << endl;
	cout << "sum of d[]=" 
    << add(d, 6) << endl;
}

▶ 이 예제는 위의 예제와는 달리 배열과 int 형의 숫자이다. 제네릭 함수에서 쓰이는 매개변수는 둘다 무조건 같은 자료형일 필요는 없다. 

 

3. 배열을 복사하는 제네릭 함수 mcopy( ) 메소드 예제 (feat. class가 2개인 template)

#include<iostream>
using namespace std;

template<class T1, class T2>
void mcopy(T1 src[], T2 dest[], int n) {
	for (int i = 0; i < n; i++) {
		dest[i] = (T2)src[i];
//T1타입의 갑을 T2타입으로 변환한다.
	}
}

void main() {
	int x[] = { 1,2,3,4,5 };
	double d[5];
	char c[5]{ 'H','e','l','l','o' };
	char e[5];

	mcopy(x, d, 5);
	mcopy(c, e, 5);
	
	for (int i = 0; i < 5; i++) {
		cout << d[i] << ' ';
	}
	cout << endl;
	for (int i = 0; i < 5; i++) {
		cout << e[i] << ' ';
	}
	cout << endl;
}

▶ 이번 예시 또한 뭔가 다르다. 이는 class가 두개이다. 서로 다른 자료형을 표시하기 위해 2개의 class를 별도로 선언을 해준 것이다. 

▶ mcopy(x,d,5) 메소드는 int형과 double 형을 표시하기 위해 서로 다른 클래스인 class T1과 T2를 선언해주었다. 

 

 

4. 배열을 출력하는 print() 템플릿 메소드의 문제점(feat. char형으로 구체화되는 경우 그래픽 문자 출력)

▶ 이 예제는 신기하다. 숫자를 char형의 배열에 입력하고 template함수로 표현하였을 때, 그래픽 문자가 출력이 된다고 한다. 

#include<iostream>
using namespace std;

template<class T>
void print(T array[], int n) {
	for (int i = 0; i < n; i++)
		cout << array[i] << "\t";
	cout << endl;
}

void main() {
	int x[] = { 1,2,3,4,5 };
	double d[5] = { 1.1,2.2,3.3,4.4,5.5 };
	print(x, 5);
	print(d, 5);

	char c[5] = { 1,2,3,4,5 };
	print(c, 5);
}

 

5. 중복함수가 템플릿 함수보다 우선임을 알려주는 예제 

#include<iostream>
using namespace std;

template<class T>
void print(T array[], int n) {
	for (int i = 0; i < n; i++) {
		cout << array[i] << "\t";
	}
	cout << endl;
}
void print(char array[], int n) {
	for (int i = 0; i < n; i++) {
		cout << (int)array[i] << "\t";
	}
	cout << endl;
}

void main() {
	int x[5] = { 1,2,3,4,5 };
	double d[5] = { 1.1,2.2,3.3,4.4,5.5 };
	print(x, 5);
	print(d, 5);

	char c[5] = { 1,2,3,4,5 };
	print(c, 5);
}

▶ 위의 예제는 print( )라는 이름의 템플릿 함수와 그냥 메소드를 선언하고, 둘 중의 어느 것을 사용하는지를 알려준다. 

▶ 컴파일링 결과, 중복함수가 먼저 실행이 됨을 알 수 있다. 

	int x[5] = { 1,2,3,4,5 };
	double d[5] = { 1.1,2.2,3.3,4.4,5.5 };
	print(x, 5);
	print(d, 5);

▶ int형의 배열과 double형의 배열이다. void 형의 print ( )메소드는 매개변수의 자료형이 char 형이기 때문에 위의 예시는 적용이 되지 않는다. 따라서 이 부분에서의 print 메소드는 템플릿 함수를 사용한다. 

	char c[5] = { 1,2,3,4,5 };
	print(c, 5);

▶ 반면 c 배열은 자료형이 char이다. 템플릿 함수에서도 char형이 가능하고 void 형태의 메소드는 char형의 배열을 매개변수로 두고 있다. 그 결과, void형의 print 메서드가 실행됨을 알 수 있다. 

 

 

제네릭 클래스(Generic Class)

▶ 템플릿 함수와 마찬가리조 클래스 멤버들을 템플릿으로 지정할 수 있다. 템플릿 클래스를 정의하는 형식은 다음과 같다.

template <템플릿 인수1, 템플릿 인수2...> 클래스의 정의

▶ 제네릭 클래스를 이용하여 스택을 만들어 보자. 만들기에 앞서 스택에 대해 간단하게 소개하도록 하겠다. 

▶ 스택(stack)은 데이터를 일시적으로 저장하기 위해 사용하는 자료구조로, 데이터의 입력과 출력 순서를 후입 선출로 가지는 형태이다. (가장 나중에 넣은 데이터를 가장 먼저 꺼내는 형식)

 

스택의 형태인 LIFO

#include<iostream>
using namespace std;

template<class T>

class MyStack {
	int tos;//top of stack
	T data[100];//T 타입의 data라는 이름을 가진 배열. 크기는 100
public:
	MyStack();
	void push(T element);//element를 data[]배열에 삽입
	T pop();//스택의 탑에 있는 데이터를 data 배열에서 return
};

template<class T>
MyStack<T>::MyStack() {
	tos = -1;//스택은 비어있음
}

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

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

void main() {
	MyStack<int>iStack;
	iStack.push(3);
	cout << iStack.pop() << endl;

	MyStack<double>dStack;
	dStack.push(3.5);
	cout << dStack.pop() << endl;

	MyStack<char>* p = new MyStack<char>();
	p->push('a');
	cout << p->pop() << endl;
	delete p;
}

▶ 

template<class T>
class MyStack {
	int tos;//top of stack
	T data[100];//T 타입의 data라는 이름을 가진 배열. 크기는 100
public:
	MyStack();
	void push(T element);//element를 data[]배열에 삽입
	T pop();//스택의 탑에 있는 데이터를 data 배열에서 return
};

▶ MyStack 제네릭 클래스의 모습이다. 매개변수는 int형의 tos와 아직 확정되지 않은 T형의 data 배열이다. 

메소드로 MyStack, void형의 push, T혀의 pop 이렇게 3개가 선언되었다. 

template<class T>
MyStack<T>::MyStack() {
	tos = -1;//스택은 비어있음
}

▶ MyStack( )메소드는 스택을 비어주게 만드는 메소드이다. tos에 -1을 대입함으로서 나중에 데이터를 입력하였을 때 0브터 시작할 수 있도록 만들어주는 것이 목적이다. 

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

▶ 다음은 T형의 element라는 이름의 변수를 가진 push 메소드이다. data의 배열의 크기를 100이라고 지정을 하였고, 배열은 맨 처음이 1이 아니라 0이기 때문에 99개의 데이터가 쌓였으면 다 차였다고 볼 수 있으므로 데이터를 넣을 수 없다. 

▶ 만약 99가 아니라면 현재 쌓여있는 tos 번째에서 하나를 추가하여 data배열에 넣을 T형의 element를 넣는다. T형은 직접 사용자가 선언하기 전에는 어떻게 쓰일지 모른다.

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

▶ pop 메소드는 가장 나중에 넣은 데이터를 출력해준다. T 형의 retData라는 변수가 가장 나중에 넣은 데이터이다. 

void main() {
	MyStack<int>iStack;
	iStack.push(3);
	cout << iStack.pop() << endl;

	MyStack<double>dStack;
	dStack.push(3.5);
	cout << dStack.pop() << endl;

	MyStack<char>* p = new MyStack<char>();
	p->push('a');
	cout << p->pop() << endl;
	delete p;
}

▶ main 함수에서는 각각의 다른 자료형을 가지고 있고 그에 따른 객체를 가지고 있다. 

 

▶ 다음은 두 개의 제네릭 타입을 가진 클래스 예제이다. 

#include<iostream>
using namespace std;

template<class T1, class T2>
//서로 다른 클래스, 즉 서로 다른 2개의 자료형 선언가능

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;
}//data1과 data2를 0으로 초기화

template<class T1, class T2>
void GClass<T1, T2>::set(T1 a, T2 b) {
	data1 = a; data2 = b;
}//data1과 data2에 사용자가 입력하는 값을 대입

template<class T1, class T2>
void GClass<T1, T2>::get(T1& a, T2& b) {
	a = data1; b = data2;
}//set에서 data1과 data2에 넣은 값을 다시 a와b에 대입

void main() {
	int a;
	double b;
    
	GClass<int, double> x;//GClass의 객체인 x
	x.set(2, 0.5);
//data1=2, data2=0.5
	x.get(a, b);
//a=2, b=0.5
	cout << "a=" << a << "," << "b=" << b << endl;

	char c;
	float d;
	GClass<char, float>y;//GClass의 객체인 y
	y.set('m', 12.5);
//data1='m', data2=12.5
	y.get(c, d);
//c='m', d=12.5
	cout << "c=" << c << "," << "d=" << d << endl;
}

728x90
반응형