본문으로 바로가기
반응형

함수템플릿

타입만 다른 함수

C++은 여러가지 개발 방식을 사용할 수 있다. C++은 C언어의 상위버전이므로 함수 위주의 구조적 기법을 쓸 수도 있고, 클래스를 활용한 객체 지향 기법도 쓸 수 있다.

이외에 임의 타입에 대해 동작하는 함수와 클래스를 작성하는 일반화 기법도 사용할 수 있다. C++ 표준 라이브러리인 STL이 일반화 기법으로 작성된 좋은 예시이다.

 

일반화는 템플릿에 의해 구현된다.

타입이 엄격한 C언어는 호환되지 않는 타입끼리 대입할 수 없어 비슷한 함수라도 타입별로 따로 만들어야하는 경우가 많다. 다음 예제는 두 변수의 값을 교환하는 swap 함수를 작성한다.

#include <stdio.h>

void swap(int& a, int& b)
{
	int t;
	t = a;
	a = b;
	b = t;
}

void swap(double& a, double& b)
{
	double t;
	t = a;
	a = b;
	b = t;
}

int main()
{
	int a = 3;
	int b = 4;

	double c = 1.2;
	double d = 3.4;

	swap(a, b);
	swap(c, d);

	printf("a = %d, b = %d\n", a, b);
	printf("c = %f, d = %f", c, d);
}

값을 변경하는 알고리즘은 같지만 임시값을 저장할 변수의 타입이 다르다.

정수와 실수의 크기나 구조가 달라 하나의 함수로 통합할 수 없고 별도의 함수를 만들어야한다.

교환 방법은 같지만 인수의 타입과 임시 변수 t의 타입이 다르다.

int, double 이에 char, long 등의 타입에 대해서도 별도의 교환함수가 필요하며 구조체나 사용자 정의형에 대해서도 각각의 교환 함수를 만들어야한다.

 

이를 해결하기위해 void * 를 사용하여 임의타입의 번지를 받아 메모리끼리 복사하는 것이다.

void * 는 임의의 대상체를 가리킬 수 있지만 대상체의 길이 정보가 없어 변수의 길이를 인수로 전달해야하는 불편함이있다.

// 파일이름 : Swapvoid.cpp
#include <stdio.h>
#include <malloc.h>
#include <memory.h>


// 프로그램이 실행될때 타입이 결정됨 => 동적바인딩
// 함수의 일반화
void swap(void *a, void *b, size_t len)
{
	void* t;
	t = malloc(len);
	memcpy(t, a, len);   // 메모리에 있는 영역을 카피하는 함수
	memcpy(a, b, len);
	memcpy(b, t, len);
	free(t);
}

int main()
{
	int a = 3;
	int b = 4;

	double c = 1.2;
	double d = 3.4;

	swap(&a, &b, sizeof(int));      // void* 타입이라 크기도 인자로 전달해야함.
	swap(&c, &d, sizeof(double));

	printf("a = %d, b = %d\n", a, b);
	printf("c = %f, d = %f", c, d);
}

Swap함수는 임의 타입 변수의 번지와 길이를 인수로 주고받는다.

인수의 길이와 같은 크기의 임시 메모리를 할당하고 이 메모리를 경유하여 값을 교환한다.

어떤 타입이 전달될지 알 수 없으므로 임시 변수도 동적할당해야하며 덕분에 배열같은 큰 데이터로 교환할 수 있다.

 

그러나 변수가 아닌 번지를 전달해야하고 길이까지 일일이 가르쳐 주어야하므로 호출부가 번잡스러워 보일 수 있다.

간단한 동작을 위해 동적으로 메모리를 할당하고 복사 후 해제까지 한다는 점에서 속도상의 불이익도 있다.

뭔가 질적으로 다른 방법이 필요하다.

 

 

함수템플릿

템플릿(Template)은 무엇인가를 만드는 형틀이다. 붕어빵을 찍어내는 빵틀이 좋은 예시라고 볼 수 있다.

모양에 대한 본을 떠 놓은 것이어서 한번만 잘 만들어 두면 이후부터 똑같은 모양을 여러 번 찍어낼 수 있다.

 

함수 템플릿은 함수를 만드는 형틀이다. 비슷한 모양의 함수가 여러 개 필요하다면 일일이 함수를 정의하는 것보다 템플릿을 먼저 만들어두고 템플릿으로부터 필요한 함수를 찍어내는 것이 효율적이다.

앞의 Swap 예제를 함수 템플릿으로 정의해보자.

// 파일이름 : Swaptemp.cpp
#include <stdio.h>

template<typename T>
void swap(T& a, T& b)
{
	T t;
	t = a;
	a = b;
	b = t;
}

int main()
{
	int a = 3;
	int b = 4;

	double c = 1.2;
	double d = 3.4;

	char e = 'e';
	char f = 'f';

	swap(a, b);
	swap(c, d);
	swap(e, f);

	printf("a = %d, b = %d\n", a, b);
	printf("c = %f, d = %f\n", c, d);
	printf("e = %c, f = %c\n", e, f);
}

swap 함수 템플릿을 정의한 후, main에서 정수, 실수, 문자에대해 각각 호출됐다.

임의 타입을 지원하므로 대입만 가능하다면 구조체나 객체 등 어떤 타입이든 잘 교환된다.

함수 템플릿 정의문은 template 키워드로 시작하며 <> 괄호 안에 타입 인수임을 의미하는 typename 키워드와 타입 인수의 이름을 적는다. 타인 인수도 명칭이므로 이름은 마음대로 정할 수 있지만, 보통은 T, A 등의 짧은 이름을 사용한다.

타입 인수는 본체에서 사용할 타입이며, 형식 인수와 비슷하다.

함수 호출부에서 int 타입을 사용하면 T는 int가되고, char를 사용하면 char타입 함수가 된다.

 

함수의 형식인수에 제한이 없는 것처럼 템플릿 인수 목록에 두 개 이상의 타입을 사용할 수도 있다.

본체에서 변화를 줄 만한 타입이 둘 이상이라면 함수 템플릿도 필요한 만큼의 타입 인수를 가진다.

이때는 원하는 만큼 인수를 지정하되 각 타입 인수의 이름은 구분 가능해야한다.

보통 T1, T2 식으로 번호를 붙이거나 T, A, R식으로 역할을 의미하는 짧은 이름을 붙인다.

template <typename T1, typename T2>

함수 템플릿 정의는 함수 호출부보다 먼저 와야한다. 예제에서는 템플릿을 main보다 앞에 정의햇는데 뒤쪽에 정의를 둘 때는 템플릿에대한 원형을 선언한다. 템플릿 함수의 원형은 일반 함수와 같되 template 키워드로부터 함수의 선언부 전체를 밝히고 본체는 생략하며 끝에 세미콜론을 붙인다.

template <typename T>
void swap(T &a, T &b);

 

 

구체화

함수 템플릿은 앞으로 만들어질 함수의 모양을 기억하는 형틀일 뿐 그 자체가 함수는 아니다.

함수가 호출될때 인수의 타입에 맞는 함수가 만들어진다. 템플릿으로부터 실제 함수를 만드는 과정을 구체화 또는 인스턴스화라고 한다. 함수 템플릿으로부터 템플릿 함수가 만들어진다.

 

컴파일러는 호출부의 인수 타입을 재료로 템플릿으로부터 함수를 찍어낸다. 템플릿 자체는 메모리를 소모하지 않아 함수를 호출하지 않으면 아무 일도 일어나지 않는다.

 

swaptemp 예제는 정수, 실수, 문자형에 대해 swap 함수를 호출한다. 컴파일러는 이때마다 swap 함수 템플릿을 참조하여 실인수의 타입에 맞는 swap 함수를 구체화한다.

템플릿을 잘 정의해 놓으면 타입별로 반복되는 부분이 통합되어 소스가 짧아지고 수정할 때 템플릿만 편집하면 되니 관리도 쉽다. 호출된 타입에대한 함수를 새로 구체화하거나 더 이상 사용하지 않으면 함수를 삭제하는 것은 컴파일러의 몫이다. 새로운 타입에 대해 swap 함수를 호출하면 해당 타입의 swap 함수가 즉시 만들어지며 호출문을 삭제하면 함수 정의문도 삭제된다.

 

명시적 인수

컴파일러는 함수 호출부의 실인수 타입을 판별하여 템플릿 함수를 구체화한다. 

앞 예제에서는 swap(a, b)는 인수가 정수이므로 swap(int, int)를 구체화하고

swap(c, d)는 인수가 실수이므로 swap(double, double) 함수를 구체화한다.

 

템플릿은 타입이 정확해야하며 swap(a, c)식으로 두 인수의 타입이 다르면 에러이다.

a는 정수이고 c는 실수인데 swap 템플릿은 두 인수의 타입이 T 하나로 결정되어 인수의 타입이 다른 함수를 구체화할 수 없다. 그나마 변수는 타입이 명확하지만 상수는 형태만으로 타입을 판별할 수 없는 경우가 많다.

// 파일이름 : TempReturn.cpp
#include <stdio.h>

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

int main()
{
	int a = max(1, 2);
	double b = max(1.1, 2.2);
	// int c = max(2, 3.14);   // 에러

	printf("c = %d\n", n);
}

max 템플릿은 두 개의 인수를 받아 큰 값을 조사한다. 

max(1, 2) 호출에 의해 max(int, int) 함수가 구체화되며 max(1.1, 2.2) 호출에 의해 max(double, double) 함수가 구체화된다. 그러나 max(2, 3.14)는 두 인수의 타입이 달라 템플릿의 구조와 맞지 않다. 2를 실수로, 또는 3.14를 정수로 변환하여 적용할수는 있지만 개발자가 무엇을 원했는지 애매하다.

 

이 문장을 정확히 컴파일하기 위해서는 실인수의 타입이 무엇인지 명확하게 밝혀야한다.

명시적으로 타입을 지정할때는 함수명 다음에 <> 괄호로 템플릿 인수의 타입을 밝힌다. 다음 두 호출문은 실인수의 타입을 명시적으로 지정했으므로 정상적으로 컴파일된다.

int c = max<int>(2, 3.14);
double d = max<double>(2, 3.14);

타입 인수를 리턴값이나 지역 변수에 적용하는 템플릿도 명시적 타입 지정이 필요하다. 리턴값이나 지역 변수는 함수 호출문에 나타나지 않아 호출문만으로 구체화할 함수를 결정할 수 없으니 어떤 함수를 원하는지 분명히 지정해야한다.

이런 경우는 흔하지않지만 객체를 대신 생성해주는 레퍼런스 함수를 만들때 가끔 사용된다.

 

// 파일이름 : tempReturn2.cpp
#include <stdio.h>

template <typename T>
T cast(int s)
{
	return (T)s;
}

int main()
{
	unsigned u = cast<unsigned>(1234);
	double d = cast<double>(5678);

	printf("u = %d, d = %f\n", u, d);
}

cast 함수는 인수로 전달된 정수형의 s를 T타입으로 캐스팅하여 리턴한다.

cast(1234)라는 호출문만으로는 어떤 타입을 리턴하는 함수를 만들지 결정할 수 없어 에러로 처리한다.

상수 123는 cast함수의 인수일뿐 T를 결정하는데 아무런 힌트가 되지 않는다.

호출문에 타입 인수가 나타나지 않아 애매하므로 명시적으로 원하는 타입을 밝혀야한다.

 

동일한 알고리즘 조건

함수 템플릿은 코드가 같고 타입만 다른 함수의 집합을 정의한다. 문제를 푸는 알고리즘이 동일해야하는데 앞에서 만들어 본 max 함수가 대표적인 예시이다.

수치형 타입은 모두 > 연산자로 대소를 비교할 수 있엇 모든 기본 타입에 대해 적용할 수 있다.

 

알고리즘이 같지않으면 코드가 달라 템플릿으로 통합할 수 없다. 두 값을 교환하는 swap 함수 템플릿은 임의의 타입에 대해 잘 동작하지만 배열에 대해서는 동작하지 않는다. 배열 두 개를 선언하고 swap 함수를 호출해보자.

int a = {1,2,3};
int b = {4,5,6};
int *pa = a;
int *pb = b;
swap(pa, pb);
// swap(a, b);

swap(pa, pb)는 컴파일은 잘 되지만 배열이 바뀌는 것이아니고 배열을 가리키는 포인터만 교환된다.

swap(a, b)는 배열이 포인터 상수여서 변경할 수 없다는 에러로 처리된다.

두 배열의 타입과 크기가 완전히 일치하더라도 swap 함수 본체에서 사용하는 = 연산이 배열에 대해 동작하지 않아 구체화할 수 없다.

 

두 배열을 바꾸려면 배열 요소를 교환해야하며 배열 크기가 가변적으로 크기도 알려주어야한다.

배열 교환을 위한 별도의 함수를 만들되 이 경우도 배열의 타입이 다양해 템플릿으로 정의하면 활용성이 높아진다.

//파일이름 : SwapArray.cpp
#include <stdio.h>
#include <malloc.h>
#include <memory.h>

template <class T>
void swaparray(T* a, T* b, int num)
{
	void* t;

	t = malloc(num * sizeof(T));
	memcpy(t, a, num * sizeof(T));
	memcpy(a, b, num * sizeof(T));
	memcpy(b, t, num * sizeof(T));
	free(t);
}


int main()
{
	int a[] = { 1, 2, 3 };
	int b[] = { 4, 5, 6 };
	char c[] = "문자열";
	char d[] = "string";

	swaparray(a, b, sizeof(a) / sizeof(a[0]));
	printf("before c = %s, d = %s\n", c, d);

	swaparray(c, d, sizeof(c) / sizeof(c[0]));
	printf("before c = %s, d = %s\n", c, d);
}

위에서 했던 swapvoid 예제와 유사하되 메모리 길이가 아닌 요소의 개수를 전달한다는 점이 다르다.

main에서 정수형 배열과 문자열 배열을 교환했으므로 두 개의 swaparray 함수가 구체화되며 배열 요소의 타입에 상관없이 잘 동작한다.

 

수치값을 교환하는 알고리즘과 완전히 달라 swaparray라는 별도의 이름을 사용했는데, 인수 목록이 달라 이 함수도 swap이라는 이름으로 정의할 수 있다. 즉, 템플릿끼리도 조건만 만족하면 오버로딩이 가능하다는 얘기이다.

 

 

임의 타입 지원 조건

템플릿은 임의의 타입에 대해 동작하므로 특정 타입에 종속적인 코드는 사용하지 말아야한다.

기본 타입을 모두 지원하는 +, -등의 연산자를 사용하거나 cout처럼 피연산자의 타입을 스스로 판별할 수 있는 코드를 사용해야할 것이다. printf처럼 타입에 따라 서식이 달라지는 함수는 안된다.

 

이 함수는 출력코드에서 %d 서식을 사용하므로 정수와 호환되는 타입만 출력할 수 있다. 범용성을 높이려면 임의의 타입을 지원하는 cout 객체를 사용해야한다. max 템플릿의 > 연산자는 기본형에 대해서는 잘 동작하지만 구조체나 객체는 > 연산자로 비교 불가능해 구체화할 수 없다.

 

연산자를 사용하는 템플릿에 대해 클래스를 지정하려면 템플릿이 사용하는 연산자를 오버로딩해야한다.

클래스가 > 연산자로 객체끼리 비교하는 기능을 제공하면 max 템플릿의 타입으로 사용할 수 있다.

다음 예제는 Human 타입의 객체끼리 교환한다.

// 파일이름 : SwapObject.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

template <typename T>
void swap(T& a, T& b)
{
	T t;
	t = a;
	a = b;
	b = t;
}

class Human
{
private:
	char* name;
	int age;

public:
	Human()
	{
		this->name = new char[1];
		name[0] = NULL;
		this->age = 0;
	}

	Human(const char* name, int age)
	{
		this->name = new char[strlen(name) + 1];
		strcpy(this->name, name);
		this->age = age;
	}

	Human(const Human& other)
	{
		this->name = new char[strlen(other.name) + 1];
		strcpy(this->name, other.name);
		this->age = other.age;
	}

	Human& operator =(const Human& other)
	{
		if (this != &other)
		{
			delete[] name;
			this->name = new char[strlen(other.name) + 1];
			strcpy(this->name, other.name);
			this->age = other.age;
		}

		return *this;
	}

	~Human()
	{
		delete[] name;
	}

	void intro()
	{
		printf("이름 : %s\n", this->name);
		printf("나이 : %d\n", this->age);
	}
};


int main()
{
	Human hong("홍길동", 10);
	Human kim("김길동", 20);

	hong.intro();
	kim.intro();
	printf("\n");

	swap(hong, kim);

	hong.intro();
	kim.intro();
	printf("\n");

}

이 코드가 제대로 동작하려면 swap 함수 내부에서 사용하는 = 대입 연산자를 Human 클래스가 지원해야한다.

예제의 Human 클래스는 복사생성자, 대입연산자를 제대로 정의하고 있기 때문에 기본 타입과 똑같이 동작하면서 swap 함수의 인수로 사용할 수 있다.

 

Human이 대입 연산자를 정의하지않으면 swap 함수로 Human 객체를 전달할때 복사생성자가 호출되어 인수 전달은 잘 수행된다. 그러나 교환을위해 t에 a를 대입하면 두 객체가 버퍼를 공유하며 이 값을 대입받는 b도 마찬가지이다.

지역 객체 t가 파괴될때 버퍼를 정리하면 b가 버퍼를 잃어버린다.

 

따라서 main이 종료될때 park의 버퍼가 이중 해제되어 다운된다. 대입 연산자가 없어도 디폴트 대입연산자가 있어 컴파일은 되지만 얕은 복사에의해 버퍼가 제대로 관리되지않아 객체 복사에서 이상이 발생한다. 함수 템플릿의 인수로 사용할 타입은 템플릿 함수의 코드와 완벽하게 호환되어야한다.

반응형