KoreanFoodie's Study

C++ 기초 개념 9-1 : C++ 템플릿, 함수 객체(Functor) 본문

Tutorials/C++ : Beginner

C++ 기초 개념 9-1 : C++ 템플릿, 함수 객체(Functor)

GoldGiver 2022. 1. 12. 15:44

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

C++ 템플릿 도입

아래와 같은 벡터 클래스를 만들었다고 하자.

class Vector {

	int* data;
	int capacity;
	int length;

public:

	Vector(int n) : data(new int[n]), capacity(n), length(0) {}

	void push_back(int input);
	void remove(int index);
	int size() { return length; }

	void print_data() { 
		for (int i = 0; i < n; ++i) {
			std::cout << *(data + i) << " " << std::endl;
		}
	}

	~Vector() { if(data) delete[] data; }
};

 

위의 벡터 클래스는 저장할 수 있는 자료형이 int 이다. 만약 char 타입이나 string 타입을 받고 싶으면 data 의 타입을 바꿔준 후, 재정의를 해야 한다. 이런 문제를 해결하기 위해, template typename 을 도입했다. 따라서 다음과 같이 수정하면 된다.

template <typename T>
class Vector {

	T* data;
	int capacity;
	int length;

public:

	// 어떤 타입을 보관하는지
	typedef T value_type;

	Vector(int n) : data(new T[n]), capacity(n), length(0) {}

	void push_back(T input);
	void remove(int index);
	int size() { return length; }

	void print_data() { 
		for (int i = 0; i < length; ++i) {
			std::cout << *(data + i) << " " << std::endl;
		}
	}

	~Vector() { if(data) delete[] data; }
};


int main() {

	int a = 5;

	Vector<int> v(a);
	v.print_data();
}

따라서 Vector<int> 를 사용할 때, T 가 int 로 치환된 클래스의 객체 v 를 생성하게 된다. 위와 같이 클래스 템플릿에 인자를 전달해서 실제 코드를 생성하는 것을 클래스 템플릿 인스턴스화(class template instantiation) 라고 한다.

템플릿이 인스턴스화 되지 않으면 컴파일 시에 아무런 코드로 변환되지 않는다.

 

 

템플릿 특수화

예를 들어, 여러 개의 타입을 사용하고 싶은데, 그 중 일부를 특정 타입으로 고정시킨다면 어떻게 써야 할까? 답은 아래와 같다.

// 기본 템플릿 정의
template <typename A, typename B, typename C>
class test1 {};

// A와 C 특수화
template <typename B>
class test2<int, B, bool> {};

// 전부 특수화
template <>
class test3<int, double, bool> {};

// 1개짜리 특수화
template<>
class test4<bool> {};

 

 

함수 템플릿 (Function template)

함수 템플릿도 템플릿 클래스와 비슷하다.

template <typename T>
T max(T& a, T& b) {
	return a > b ? a : b;
}


int main() {

	int i1 = 1;
	int i2 = 2;

	std::string s1 = "hello";
	std::string s2 = "world";

	std::cout << "Max is : " << max(i1, i2) << std::endl;
	std::cout << "Max is : " << max(s1, s2) << std::endl;

}

 

 

함수 객체 (Function Object - Functor) 의 도입

아래와 같은 sort 함수를 만드는 과정에서 순서를 역순으로 만들고 싶다면 어떻게 해야할까?

template <typename Cont>
void bubble_sort(Cont& cont) {
	for (int i = 0; i < cont.size()-1; ++i) {
		for (int j = i+1; j < cont.size(); ++j) {
			if (cont[i] > cont[j]) {
				cont.swap(i, j);
			}
		}
	}
}

 

일단, 역순으로 sort 하는 함수를 따로 만들거나, 부등호(<)를 오버로딩할 수도 있다. 하지만 이미 만들어져 있는 클래스(e.g. std::string)의 경우에는 그게 불가능하다. 함수 객체(Function Object, Functor)를 사용하면 이를 쉽게 해결할 수 있다.

template <typename Cont, typename Comp>
void bubble_sort(Cont& cont, Comp& comp) {
	for (int i = 0; i < cont.size()-1; ++i) {
		for (int j = i+1; j < cont.size(); ++j) {
			if (!comp(cont[i], cont[j])) {
				cont.swap(i, j);
			}
		}
	}
}

struct Comp1 {
	bool operator()(int a, int b) { return a > b; }
};

struct Comp2 {
	bool operator()(int a, int b) { return a < b; }
};

int main() {

	Vector<int> v(5);

	v.push_back(4);
	v.push_back(1);
	v.push_back(3);
	v.push_back(2);
	v.push_back(5);

	std::cout << "정렬 전" << std::endl;
	v.print_data();

	std::cout << "내림차순 정렬 후" << std::endl;
	Comp1 comp1;
	bubble_sort(v, comp1);
	v.print_data();

	std::cout << "오름차순 정렬 후" << std::endl;
	Comp2 comp2;
	bubble_sort(v, comp2);
	v.print_data();
}

 

이때, bubble_sort 에서 비교 함수로 사용하는 것은 그냥 함수가 아닌, 함수 객체이다. 또한, bubble_sort 작동을 위해 [ ] 연산자와 swap, size 멤버 함수를 만들어 주어야 한다.

위의 comp1, comp2 처럼, 함수는 아니지만 함수인 척을 하는 객체를 Function Object, 줄여서 Functor 라고 부른다.

 

실제로 bubble_sort(v) 를 하면 첫 번째 버전이, bubble_sort(v, comp) 을 실행하면 두 번째 버전(Functor가 있는 버전)이 인스턴스화되어 실행된다.

Functor를 사용하면 기능 조작이 쉬우며, 컴파일러가 operator( ) 자체를 인라인화 시켜서 매우 빠르게 작업을 수행할 수 있다!

 

 

타입이 아닌 템플릿 인자 (non-type template arguments)

다음과 같이, 타입이 아닌 인자를 템플릿화할 수도 있다.

template <typename T, int num>
T add(T& a) {
	return a + num;
}

int main() {
	int temp = 10;
	std::cout << add<int, 5>(temp) << std::endl;
}

 

위처럼, add를 사용할 때는 템플릿 인자를 < > 로 지정해야한다.

 

템플릿 인자로 전달할 수 있는 타입은 다음과 같다 :

1. 정수 타입들 (bool, char, int, long). float 과 double 은 제외.

2. 포인터 타입

3. enum 타입

4. std::nullptr_t (널 포인터)

 

템플릿 인자를 활용하는 예시로 배열이 있다.

#include <array>

int main() {
	std::array<int, 5> arr = {1, 2, 3, 4, 5};
	// int arr[5] = {1, 2, 3, 4, 5}; 와 동일
}

 

 

디폴트 템플릿 인자

템플릿도 디폴트 인자를 설정할 수 있다. 아래는 기본으로 5를 더해주는 함수이다.

template <typename T, int num = 5>
T add(T a) {
    return a + num;
}

 

타입도 마찬가지로, 기본 타입을 설정해 줄 수 있다. 예를 들어, 최솟값을 리턴하는 함수를 Comp라는 템플릿의 Functor를 이용해서 구한다고 하면, 다음과 같은 코드를 짜볼 수 있다.

template <typename T>
struct Compare {
	T operator()(T a, T b) {
		return a > b ? b : a;
	}
};

template <typename T, typename Comp>
T min(T a, T b) {
	Comp comp;
	return comp(a, b);
}

int main() {

	int a = 10;
	int b = 20;

	Compare<int> compare;

	std::cout << min<int, Compare<int>>(a, b) << std::endl;

}

 

하지만, min<int, Compare<int>> 를 사용하는 대신, 템플릿 타입에 기본값을 부여하여 이를 간단화할 수 있다.

template <typename T>
struct Compare {
	bool operator()(T& a, T& b) const { return a < b; }
};

template <typename T, typename Comp = Compare<T>>
T min(T a, T b) {
	Comp comp;
	if (comp(a, b)) {
		return a;
	}
	return b;
}

int main() {

	int a = 10;
	int b = 20;

	Compare<int> compare;

	std::cout << min(a, b) << std::endl;
}

 

위의 템플릿 클래스의 Vector를 이용해서 2차원, 3차원 벡터를 만들어 보면 다음과 같다.

// 코드 간소화 함
template <typename T>
class Vector {

	T* data;
	int capacity;
	int length;

public:

	// 어떤 타입을 보관하는지
	typedef T value_type;

	Vector() {}

	Vector(int n) : data(new T[n]), capacity(n), length(0) {}

	void push_back(T input) { data[length++] = input; }

	T operator[](int i) { return data[i]; }

	// 소멸자 때문에 에러가 발생한다.
	// main 에서, v 를 먼저 해제하고,
	// 그 후 vv 를 해제하기 때문으로 보인다.
	// ~Vector() { if(data) delete[] data; }
};

int main() {

	Vector<Vector<int>> v(3);
	Vector<int> vv(4);
	// Vector<int>* vp = new Vector<int>[4];
	v.push_back(vv);
}

 

 

 

 

Comments