새로운 용어가 나오면 역시 단어뜻부터 살펴 봐야겠죠
단어의 의미는 본을 뜰 수 있는 형판, 틀과 같은것을 의미 합니다. 그림을 그릴때는 대고 따라 그릴 수 있는 어떤 틀이 되겠네요.
C++은 혼합형 언어라고 이야기 합니다. 개발 방식 (패러다임)에 따라 분류 했을 때 그러합니다. 멀티패러다임이라고도 합니다.
C언어를 계승해서 발전된 형태이기 때문에 C언어에서 사용되던 패러다임인 절차형 프로그래밍, 함수형 프로그래밍도 사용 가능하고, 객체지향 패러다임도 사용할 수 있습니다.
이런 패러다임 중에서 타입에 관계없이 동작하는 함수와 클래스를 작성하는 기법을 일반화 기법이라고 하는데, 이 일반화 기법을 템플릿을 통해서 효율적으로 구현할 수 있게 됩니다.
먼저 템플릿을 사용하지 않고 일반화를 구현하는 방법이 어떤 방법이 있는지 알아 보겠습니다.
1. 함수오버로딩으로 구현한 일반화 기법입니다.
#include <iostream>
using namespace std;
void swap(int& a, int& b)
{
int temp;
temp = a; a = b; b = temp;
}
void swap(float& a, float& b)
{
double temp;
temp = a; a = b; b = temp;
}
int main()
{
int a = 1, b = 2;
float c = 1.2, d = 3.4;
swap(a, b);
swap(c, d);
cout << "a = " << a << ", " << "b = " << b << endl;
cout << "c = " << c << ", " << "d = " << d << endl;
}
위치를 바꾸는 swap 함수를 오버로딩 시켜 int 타입 뿐만 아니라 float 타입에서도 작동하도록 구현하였습니다.
다루어야 하는 자료형이 적을 때는 훌륭한 방법입니다만 대규모 프로젝트라면
자료형이 수십개가 추가될 지도 모르는 일입니다.
그때마다 수많은 자료형에 대한 코드를 계속 오버로딩 시켜 구현해야 하는 불편함이 있지요.
그뿐만 아니라 알고리즘을 수정해야 하는 상황이 생긴다면 어떨까요?
99개의 오버로딩된 함수가 있다면 함수 내의 알고리즘을 99번 수정해야 할 것입니다.
많으면 많을 수록 비 효율적인 방법이 될 수 밖에 없습니다.
2. #define 매크로를 사용하는 방법
#define SWAP(Temp,a,b){Temp temp;temp=a;a=b;b=temp;}
#include <iostream>
using namespace std;
void swap(int& a, int& b)
{
int temp;
temp = a; a = b; b = temp;
}
void swap(float& a, float& b)
{
double temp;
temp = a; a = b; b = temp;
}
int main()
{
int a = 1, b = 2;
float c = 1.2, d = 3.4;
swap(a, b);
swap(c, d);
SWAP(int, a, b);
SWAP(float, c, d);
cout << "a = " << a << ", " << "b = " << b << endl;
cout << "c = " << c << ", " << "d = " << d << endl;
}
매크로 함수를 사용해서 자료형에 관계없이 단순 치환작업으로 동작하게 할 수 있지만 그때 그때 마다 타입을 매개변수로전달해야하고, 디버깅이 어려운점 등 매크로함수가 가지는 한계를 그대로 가지고 있습니다.
3. void 포인터를 사용, 자료형이 정해지지 않은 자료 그 자체와 데이터의 크기만 받아와서 형변환을 시키는 방법이다.
#include <iostream>
using namespace std;
void swap(void* a, void* b, int len)
{
//void 포인터끼리 형변환을 시킨다.
void* temp;
temp = new void*;
memcpy(temp, a, len);
memcpy(a, b, len);
memcpy(b, temp, len);
delete(temp);
}
int main()
{
int a = 1, b = 2;
float c = 1.2, d = 3.4;
swap(&a, &b, sizeof(int));
swap(&c, &d, sizeof(float));
cout << "a = " << a << ", " << "b = " << b << endl;
cout << "c = " << c << ", " << "d = " << d << endl;
}
void* 는 어떤 타입의 데이터도 될 수 있습니다.
메모리의 길이값을 알려주고 memcpy를 활용해서 메모리끼리 값을 복사했다. 이런 방법으로 swap을 타입에 관계없이 동작하도록 만들었어요.
1, 2번 방법의 한계를 많이 극복했지만, 매개변수로 변수를 전달할 수 없고, 주소값을 직접 전달해야 하며, 길이값도 넣어줘야 하고, 포인터를 직접 다루는데에 대한 위험성도 생각해야 할 것입니다.
또한 동적 할당만 사용할 수 있기 때문에 시간이 오래걸린다는 단점이 있습니다.
이제 Template을 한번 써보겠습니다.
Template을 사용한 방법
#include <iostream>
#include <string>
using namespace std;
template<typename T>
void myswap(T& a, T& b)
{
T temp;
temp = a; a = b; b = temp;
}
int main()
{
int a = 1, b = 2;
float c = 1.2, d = 3.4;
char e = 'e', f = 'f';
string g = "안녕하세요.", h = "반갑습니다.";
myswap(a, b);
myswap(c, d);
myswap(e, f);
myswap(g, h);
cout << "a = " << a << ", " << "b = " << b << endl;
cout << "c = " << c << ", " << "d = " << d << endl;
cout << "e = " << e << ", " << "f = " << f << endl;
cout << "g = " << g << ", " << "h = " << h << endl;
}
std 라이브러리에 swap 이라는 함수가 이미 존재하고 있어서 이름을 swap으로 만들면 E0308 에러를 던집니다.
이럴땐 swap 함수의 이름을 위처럼 변경하거나
std 네임스페이스를 사용하지 않으면 됩니다.
#include <iostream>
#include <string>
template<typename T>
void swap(T& a, T& b)
{
T temp;
temp = a; a = b; b = temp;
}
int main()
{
int a = 1, b = 2;
float c = 1.2, d = 3.4;
char e = 'e', f = 'f';
std::string g = "안녕하세요.", h = "반갑습니다.";
swap(a, b);
swap(c, d);
swap(e, f);
swap(g, h);
std::cout << "a = " << a << ", " << "b = " << b << std::endl;
std::cout << "c = " << c << ", " << "d = " << d << std::endl;
std::cout << "e = " << e << ", " << "f = " << f << std::endl;
std::cout << "g = " << g << ", " << "h = " << h << std::endl;
}
네임스페이스를 사용하지 않고 std 를 직접 스코프함
예상대로 잘 동작하는것을 볼 수 있습니다.
템플릿은 이렇게 사용합니다.
template <typename T>
T Functionname(T a, T b)
{
구현부 안에도 T를 써서 구현하면 모든 T를 전달된 자료의 자료형으로 간주하여 연산합니다.
}
먼저 template 예약어로 이제부터 템플릿을 사용하겠다고 알리면 컴파일러는 이제 템플릿을 쓰겠구나 라고 알아듣습니다.
그다음으로 꺽쇠가 필요합니다. '<>' (영어로는 Right/Left Angle Bracket 이라고 합니다.) 를 사용하여 typename T라고 선언 하면 함수를 호출할 때 전달되는 타입을 T로 인식하겠다는 의미입니다. 여기서 typename 또한 키워드입니다.
붕어빵을 만드는데 붕어빵 재료가 슈크림, 팥, 치즈가 있다고 합시다. 붕어빵을 맛있게 만들기 위해서는 붕어빵의 재료마다 붕어빵틀의 종류가 달라야 한다고 가정합시다. 어떤 재료는 열전도율이 높은 틀을 써야 하고 어떤 재료는 특별하게 코팅된 틀을 써서 타지 않게 해야 합니다.
<typename T> 는 템플릿은 들어온 재료를 보고 틀의 종류를 판단하겠다는 의미입니다. 결정된 틀의 종류를 T 라고 define을 통해 치환하겠다고 이해해도 무방합니다.
그다음 줄에서 맨앞에오는 T는 함수 반환값의 타입이며, 예제에서는 void로 활용하였습니다. 여기서 T를 쓸 수도 있다는 이야기입니다.
이때 꺽쇠 안에 typename 은 여러개 만들 수 있는데, 각각 n번째 인수의 타입을 의미 합니다.
<typename T1, typename T2> 이런식으로 T1, T2는 식별자로서 구분해주어야 합니다.
템플릿함수의 정의는 C언어에서 전역함수 정의와 같이 호출부보다 먼저 와야 합니다.
정의부가 뒤에 오려면 마찬가지로 앞쪽에서 선언이 먼저 이루어져야 하죠. 이 부분은 C언어에서 전역함수와 완전히 동일합니다.
구체화
템플릿의 장점
함수 템플릿은 함수의 모양을 만들어놓은 틀입니다. 그자체가 함수는 아니라는 이야기죠.
함수가 호출 될 때, 템플릿을 기반으로 해서 인수의 타입에 맞는 함수가 새로 만들어 지는 것입니다.
이렇게 템플릿으로 부터 실제하는 새로운 함수가 만들어지는 과정을 인스턴스화 라고 합니다.(Instantiation)
함수 템플릿을 기반으로 템플릿 함수가 만들어 지는 것입니다.
컴파일러가 함수 호출 당시에 인수로 넘겨진 데이터의 타입을 확인해서 템플릿을 기반으로 함수를 만들어 냅니다.
템플릿 자체로는 메모리가 전혀 할당되지 않는 것입니다. 이는 먼저 살펴봤던 일반화 사례들과 비교했을때 아주 커다란 장점입니다.
또한 반복되는 소스코드를 통합적으로 관리할 수 있어, 유지 보수에서 아주 큰 메리트가 있습니다.
메모리 관리도 완전히 컴파일러에게 맡겨 버리면 되기 때문에 프로그래머가 고려할 작업이 줄죠.
템플릿 함수는 함수호출때 생성 되는게 아니라 매크로 함수와 같이 컴파일단계에서 미리 만들어집니다. 실행시간에서 손해보는 부분도 없어지죠.
템플릿의 단점
템플릿으로 소스는 간편하게 관리 할 수 있지만 다른 타입을 만날 때마다 인스턴스화 되어 메모리가 할당되기 때문에 실행파일의 크기는 커집니다. 컴퓨터 공학에서 크기와 시간은 항상 반비례 관계라는것을 알고 있어야 합니다.
많은 자료형을 처리하면서 동시에 메모리를 절약하고 싶다면 템플릿 보다는 위에서 먼저 알아 보았던 '3. void 포인터 사용' 방법으로 임의로 메모리를 할당/해제하는 방법이 더 유리합니다.
명시적 템플릿 인수
컴파일러는 함수 호출부를 확인하여 -위의 예제에서는 swap(a, b);- 넘겨진 인수의 타입을 판별합니다.
int 타입이 넘겨졌다면 그 타입을 바탕으로 템플릿 함수를 인스턴스화 합니다.
템플릿은 타입이 정확해야 합니다. 넘겨진 두 인수의 타입이 다르다면 에러가 납니다.
변수는 타입을 정확히 알수 있지만 상수는 어떨까요?
1이라는 상수와 1.1이라는 상수를 넣는다면요? 당연히 에러가 납니다.
이럴때 계산을 하고싶다면 캐스팅이 필요합니다. int로 통일하는 DownCasting 이건, float으로 통일하는 UpCasting 이건 둘중하나를 해줘야 하죠.
자료를 더한 결과를 반환하는 간단한 템플릿을 만들어 보았습니다.
#include <stdio.h>
template <typename T>
T max(T a, T b)
{
return a + b;
}
int main()
{
int i = 1;
float f = 1.9;
int result = max(i, f);
printf("result = %d\n", result);
}
int result = max(i, f);
위 행에서 에러가 발생합니다. i의 타입은 int 이고, f의 타입은 float인데 타입이 다른 변수끼리 더하고 있죠.
이때 템플릿인수의 타입을 <> 를 사용해서 명시해주면 해당 자료형으로 캐스팅한 상태로 컴파일러 오류 없이 계산을 완료할 수 있게 됩니다.
int result = max<int>(i, f);
위와 같이 바꿔 주면 정상적으로 동작하는것을 볼 수 있습니다.
처음 썼던 예제 그대로 swap 함수를 활용하려 하면 매개변수로 레퍼런스를 받고 있기 때문에 레퍼런스의 성질
(레퍼런스는 레퍼런스의 타입과 일치하는 변수에만 바인딩될 수 있습니다.)
에 따라서 함수 도입부 부터 형변환이 이루어질 수 없고, 컴파일러가 에러를 던지게 됩니다.
명시적 인수
인수의 타입을 반드시 명시해야만 템플릿 함수를 사용할 수 있게 만들자
#include <iostream>
using namespace std;
template <typename T>
T cast(int s)
{
return (T)s;
}
int main()
{
float f = cast<float>(1234);
double d = cast<double>(5678);
//다음 라인은 컴파일 에러를 일으킵니다.
//float f = cast(1234);
//double d = cast(5678);
cout << "f = " << f << ", " << "d = " << d << endl;
cout << sizeof(f) << ", " << sizeof(d) << endl;
}
위의 예제에서 템플릿 함수인 cast 함수를 살펴 보겠습니다. 매개변수 s를 정수형으로 받습니다.
매개변수를 int 라고 명시를 해버렸네요? 이렇게 되면 컴파일러는 T의 타입을 어떻게 판단해야 하죠?
일반적인 형태로 인수만 받아서 사용(func(arg) 형태) 하게 될 경우 T의 타입을 판단할만한 근거가 전혀 없어지게 됩니다.
그래서 주석처리한 부분은 컴파일 에러가 발생하는 겁니다. 템플릿 인수T 의 타입을 결정하기 위해서는 힌트가 필요해요함수 호출 부에서 <자료형> 으로 반드시 명시해주어야만 에러가 발생하지 않습니다.이를 명시적 인수 지정 기법이라고 합니다.
명시적 인수 지정 기법은 템플릿함수가 너무 많이 생성되는 것을 방지하는 역할을 합니다.
특정한 타입에 대해서만 인스턴스화 할 수 있도록 호출 할 때 마다 원하는 타입을 명시적으로 지정하도록 유도하는 것입니다.
인수의 타입을 명시적으로 지정해주는 작업이 왜 필요한지 알아보겠습니다.
#include<iostream>
using namespace std;
template<typename T>
void Func(T arg)
{
cout << "이것은 매우 길고 메모리 사용량이 많은 함수다." << endl;
}
int main()
{
int i = 10;
unsigned u = 20;
short s = 30;
char c = 40;
Func(i);
Func(u);
Func(s);
Func(c);
}
#include<iostream>
using namespace std;
template<typename T>
void Func(T arg)
{
cout << "이것은 매우 길고 메모리 사용량이 많은 함수다." << endl;
}
int main()
{
int i = 10;
unsigned u = 20;
short s = 30;
char c = 40;
Func<int>(i);
Func<int>(u);
Func<int>(s);
Func<int>(c);
}
두 예제 코드는 int 로 인수를 명시 했는지 안했는지 차이만 있을 뿐입니다.
동작 자체는 두 코드 모두 정상적으로 작동하는것을 볼 수 있습니다만 첫번째 예제에서 Func(i), Func(u), Func(s), Func(c)는 각각 타입별로 호출시 마다 인스턴스화 되어 메모리가 많이 필요하게 됩니다. 결국 실행파일도 커지게 되는 거예요.
그런데 위 4가지 자료형의 경우 int 자료형 하나로 통일해서 처리해도 문제가 없다고 볼 수 있죠? 이런 경우에 명시적 인수를 사용해서 메모리를 절약 할 수 있다는 겁니다.
결과적으로 위의 예제보다 아래의 예제에서 템플릿 함수에 할당되는 메모리 양이 4분의1로 줄어들게 됩니다.
template<typename T>
template<class T>
위 두문장은 문맥적으로 동일하게 취급되며, 실제적인 차이가 없다고 보시면 됩니다.
객체지향 언어를 조금이라도 공부하셨던 분이라면 type과 class는 차이가 없다는것을 이미 알고 계실 겁니다.
여기서는 두 문장이 문법적인 차이가 없다는 점만 설명하고 넘어 가겠습니다.
'C++문법 공부' 카테고리의 다른 글
맴버 함수 사용시 const 키워드의 사용 (0) | 2023.07.10 |
---|---|
2차원 배열과 포인터의 이해 (0) | 2023.07.05 |
소멸자의 virtual 선언 (0) | 2023.06.30 |
가상함수 Virtual과 오버라이딩 (0) | 2023.06.27 |
포함 (0) | 2023.06.27 |