클래스 템플릿
타입만 다른 클래스
함수 템플릿은 비슷한 함수를 찍어내는 데 비해 클래스 템플릿은 구조나 알고리즘은 같되 멤버의 타입이 다른 클래스를 찍어내는 틀이다. 다음 클래스들은 화면의 특정 위치에 값 하나를 출력하는데 타입별로 클래스를 일일이 만들었다.
private:
int x, y;
double value;
public:
PosValue(int ax, int ay, double av) : x(ax), y(ay), value(av) {}
void outvalue();
};
출력 좌표를 지칭하는 x, y는 모두 정수형으로 같지만 출력값인 value는 대상값의 종류에 따라 타입이 다르다.
value의 타입이 제각각이라 이 값을 초기화하는 생성자의 원형도 다르며, 클래스는 오버로딩이 지원되지 않아 고유한 이름을 붙였다. 실제로 다른 부분은 value와 관련된 부분밖에 없고 나머지는 모두 동일하므로 하나의 템플릿으로 통합할 수 있다.
// 파일이름 : PosValueTemp.cpp
#include <iostream>
#include "cursor.h"
using namespace std;
template <typename T>
class PosValue
{
private:
int x, y;
T value;
public:
PosValue(int x, int y, T v) : x(x), y(y), value(v) { }
void outvalue()
{
gotoxy(x, y);
cout << value << endl;
}
};
int main()
{
PosValue<int> iv(10, 10, 2);
PosValue<char> cv(25, 5, 'C');
PosValue<double> dv(30, 15, 3.14);
iv.outvalue();
cv.outvalue();
dv.outvalue();
}
클래스 선언문 앞에 template <typename T>를 붙이고 value 멤버의 타입과 생성자의 세번째 인수에 T타입을 적용했다.
템플릿을 정의해 놓으면 비슷한 클래스를 손쉽게 찍어낼 수 있다. main에서 3개의 객체를 생성하여 화면에 출력한다.
템플릿 클래스의 타입명에는 언제나 <> 괄호가 함께 따라다니며 객체를 선언할때 <> 괄호 안에 원하는 타입을 밝힌다.
PosValue는 템플릿 이름일뿐 클래스가 아니어서 이 이름으로는 객체를 생성할 수 없다.
value가 int인 클래스 이름은 PosValue<int>이고 value가 char인 클래스의 이름은 PosValue<char>이다.
객체를 선언할 때는 <> 괄호와 타입까지 반드시 밝혀야한다.
객체 선언문의 인수타입으로 유추한다면 PosValue iv(10, 10, 2)의 마지막 인수가 정수이므로 PosValue<int> 객체를 생성하면 될 것 같지만 여러 벌로 오버로딩된 경우 애매함이 발생할 수 있다. 객체를 초기화하기 전에 메모리부터 할당해야하는데 생성자 호출 이전에 크기를 계산해야하므로 클래스 이름에 타입이 분명히 명시되어야한다.
부모클래스로 사용된 클래스는 설사 타입의 객체를 생성하지않더라도 즉시 구체화된다.
부모의 모습이 결정되어야 자식 클래스를 정의할 수 있기 때문이다.
템플릿 멤버 함수
템플릿은 함수를 만들때도 사용되고 클래스를 만들때도 사용된다.
클래스에 속한 멤버 함수도 일종의 함수이므로 템플릿으로 선언할 수 있다. 일반 클래스 소속이더라도 멤버 함수가 타입에 따라 여러 벌 필요하다면 멤버 함수만 템플릿으로 만든다.
방법은 위에서 봤던 함수 템플릿과 같되 클래스에 소속되어있다는 것만 다르다.
// 파일이름 : TempMember.cpp
#include <stdio.h>
class Util
{
public:
template<typename T>
void swap(T& a, T& b)
{
T t;
t = a;
a = b;
b = t;
}
};
int main()
{
Util u;
int a = 3;
int b = 4;
double c = 1.2;
double d = 3.4;
char e = 'e';
char f = 'f';
u.swap(a, b);
u.swap(c, d);
u.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);
}
Util 클래스에 여러 타입의 값을 교환하는 함수가 필요하다면 swap 함수를 템플릿으로 정의한다.
Util 객체 u를 선언한 후 u의 멤버 함수 swap을 세 가지 타입에 대해 호출했다.
컴파일러는 호출되는 타입별로 멤버 함수를 만든다.
이런식으로 하다보면 Util 클래스의 멤버 개수는 가변적이다. 위 코드에서 Util 클래스의 멤버 함수는 세 개이지만 다른 타입에 대해 swap을 호출하면 Util 클래스가 계속 확장된다. 다행히 멤버 함수의 개수는 객체의 크기에 영향을 미치지 않는다.
디폴트 템플릿 인수
함수의 디폴트 인수는 호출시 생략된 인수에 기본적으로 적용되는 값이다. 클래스 템플릿에도 비슷한 개념인 디폴트 타입 인수가 있다. 템플릿 선언문의 타입 인수 다음에 = 구분자를 쓰고 기본 타입을 명시한다. 다음 코드는 PosValue의 기본 타입을 int로 지정한다.
template <typename T=int>
class PosValue
{
...
이 템플릿으로부터 객체를 생성할때 별다른 타입 지정이 없으면 T에는 디폴트 타입인 int가 적용된다.
정수형의 PosValue 객체를 선언할 때는 타입 지엉없이 빈 <> 괄호만 쓴다.
PosValue<> iv(10, 10, 2);
디폴트 타입을 사용하더라도 빈 괄호 <>는 꼭 있어야한다. 디폴트는 어디까지나 생략이 기본값일 뿐이므로 PosValue<double>처럼 타입을 명시하면 무시된다. 타입 인수가 여러 개일 때 오른쪽 인수부터 차례대로 디폴트를 지정할 수 있으며 객체를 선언할 때도 오른쪽부터 순서대로 생략 가능하다.
클래스 템플릿과는 달리 함수 템플릿에는 디폴트 인수를 지정할 수 없다. 함수는 호출할 때 실인수의 타입을 보고 구체화할 함수를 결정하는데 실인수를 생략해버리면 어떤 타입의 함수를 원하는지 알 방법이 없기 때문이다.
비타입 인수
템플릿의 인수는 통상 타입을 지정하지만 상수를 전달하기도 하는데 이를 비타입 인수라고한다. 다음 예제의 Array 클래스는 임의 타입의 고정 크기 배열을 표현하며 값을 읽거나 변경하는 기능을 제공한다. 배열 요소의 타입을 지정하는 타입 인수와 배열의 크기를 지정하는 정수 상수를 전달받는다.
// 파일이름 : NonTypeArgument.cpp
#include <stdio.h>
template <typename T, int N>
class Array
{
private:
T ar[N];
public:
void SetAt(int n, T v)
{
if (n < N && n >= 0)
{
ar[n] = v;
}
}
T GetAt(int n)
{
return (n < N&& n >= 0 ? ar[n] : 0);
}
};
int main()
{
Array<int, 5> ari;
ari.SetAt(1, 1234);
ari.SetAt(1000, 5678);
printf("%d\n", ari.GetAt(1));
printf("%d\n", ari.GetAt(5));
}
배열을 래핑한 클래스이되 요소를 액세스 하기 전에 범위를 점검하여 치명적인 에러를 방지한다. 범위를 벗어나는 첨자를 사용하면 무시하거나 0을 리턴한다. Array<int, 5> 타입은 다음과 같은 클래스로 구체화된다.
class
{
private:
int ar[5];
public:
void SetAt(int n, int v) { if (n < 5 && n >= 0) ar[n] = v;}
int GetAt(int n) {return (n < 5 && n >= 0 ? ar[n] : 0); }
};
첫번째 인수로 int타입을 전달하고 두번째인수로 상수 5를 전달하여 크기 5의 정수형 배열을 생성했다.
두번째 인수는 타입이 아닌 값이어서 비타입인수라고한다. 정수만 사용할 수 있으며 실수나 문자열 등의 복잡한 값은 사용할 수 없다. 이 템플릿은 임의 타입에 대해 임의 크기를 지원하는 안전 배열을 만든다.
템플릿을 쓰는 대신 생성자의 인수로 크기를 전달하고 동적으로 할당하는 방법도 가능하다. 포인터를 사용하면 필요한 만큼 할당할 수 있고 원할경우 재할당도 가능하다. 하지만 동적 할당을 하면 소멸자, 복사생성자, 대입 연산자를 모두 정의해야하며 상속 관계까지 고려하면 모든 함수는 가상으로 선언해야한다. 범용적이고 신축적이지만 코드의 부담이 크다.
템플릿은 정적 할당하면서도 객체마다 크기를 다르게 생성할 수 있어 간편하며 속도도 빠르고 안전하다.
Array 템플릿은 비타입 인수로 크기를 지정할 수 있고 구조가 단순해서 좋다. 그러나 크기가 다른 객체를 생성할때마다 클래스가 구체화된다는 면에서 낭비가 있다. 크기가 다르면 아예 다른 타입이어서 서로 호환되지 않는다.
Array<int, 5> ari;
Array<int, 5> ari2;
Array<int, 6> ari3;
ari = ari2;
ari = ari3; // 에러
크기 5의 배열과 크기 6의 배열은 객체 크기부터 달라 대입할 수 없다. 클래스 선언문의 비타입 인수는 상수만 사용할 수 있으며 실행 중에 결정되는 변수는 사용할 수 없다.
int size = 5;
Array<int, size> ari;
템플릿은 타입 인수를 적용하여 컴파일중에 클래스를 만드는 형틀이므로 모든 정보가 컴파일중에 결정되어야한다.
템플릿 클래스는 실행 중에 생성되는것이 아니어서 컴파일 시점에 값이 결정되는 변수는 사용할 수 없다.
함수 템플릿에도 비타입 인수를 사용할 수 있되 형식인수 목록에 상수가 올 수 없으므로 비타입 인수는 본체에서만 사용할 수 있다. 비타입 인수는 함수 호출시에 인수로 전달되는 것이 아니어서 함수명 다음에 sub<5>(); 식으로 인수의 값을 명시적으로 지정한다. 함수 템플릿의 비타입 인수는 실용성이 떨어지며 일부 컴파일러는 지원하지 않는다.
템플릿 고급
명시적 구체화
개발자가 원하는 타입으로 템플릿 함수를 호출하면 나머지는 컴파일러가 알아서 처리한다.
컴파일러는 호출부를 보고 필요한 함수를 생성하는데 이를 암시적 구체화라고 한다. 호출하지 않은 타입에 대해서는 사용할 일이 없으니 함수를 만들 필요가 없다.
그러나 호출하지 않아도 미리 함수를 만들어 놓아야할 필요가 있는데 이를 명시적 구체화라고한다.
호출 여부에 상관없이 지정한 타입에 대해 함수를 만들 것을 컴파일러에게 지시하는 것이다.
예를들어 float 타입을 교환하는 함수를 생성하고 싶다면 다음 명령을 사용한다.
template void swap<float>(float, float);
키워드 template 다음에 함수 이름과 적용할 타입을 밝히면 컴파일러가 이 타입으로 함수를 미리 생성해 놓는다.
템플릿 모양을 알아야 함수를 만들 수 있으므로 명시적 구체화 선언문은 템플릿 선언보다 더 뒤에 와야한다.
클래스 템플릿도 함수 템플릿과 마찬가지로 특정 타입에 대해 미리 클래스를 생성해 놓으려면 명시적으로 구체화해야한다. 다음 코드는 float 타입의 PosValue 클래스를 생성하며 객체를 선언하지 않아도 클래스 선언과 멤버 함수가 구체화된다.
template class PosValue<float>;
클래스가 정의되어 있으니 사용자는 언제든지 PosValue<float>타입의 객체를 생성할 수 있다.
특수화
같은 템플릿으로부터 생성된 함수는 타입만 다를 뿐 본체 코드가 같으니 동작도 같다.
만약 특정 타입에 대해 약간 다르게 동작하는 함수를 만들고 싶다면 해당 타입에 대해 별도의 함수를 정의할 수 있는데, 이를 특수화(Specialization)라고 한다.
두 값을 교환하는 swap함수를 실수에 대해서는 정수부만 교환하도록 정의하고 싶다고 가정해보자.
이때는 double 형에 대한 swap 함수를 특수하게 따로 정의한다.
// 파일이름 : Specializtion.cpp
#include <stdio.h>
template<class T>
void swap(T& a, T& b)
{
T t;
t = a;
a = b;
b = t;
}
template <> void swap<double>(double& a, double& b)
{
int i, j;
i = (int)a;
j = (int)b;
a = a - i + j;
b = b - j + i;
}
int main()
{
double a = 1.2, b = 3.4;
printf("a = %g, b = %g\n", a, b);
swap(a, b);
printf("a = %g, b = %g\n", a, b);
}
임의 타입에 동작하는 swap 템플릿을 정의하고 double 형에 대해서는 특수한 swap 함수를 별도로 정의한다.
main에서 두 개의 실수를 swap 함수로 전달하여 교환하는데 일반적인 swap 함수 대신 실수에 대해 특수화된 swap 함수가 호출된다. a, b 정수부만 바뀌며 실수부는 그대로 유지된다.
컴파일러는 임의 타입에 대해 적용되는 템플릿보다 특수화된 템플릿에 더 우선권을 주어 타입이 맞는 툭수화 템플릿이 있으면 이 템플릿으로부터 함수를 생성한다. doublㄷ에 대해 특수한 템플릿이 정의되어있어 실수에 대해서는 아래쪽의 템플릿을 사용한다. 이 템플릿이 없으면 임의 타입에 대해 동작하는 swap 함수가 호출되어 정수부와 실수부가 모두 바뀐다.
특수화 템플릿의 표기법이 다소 어렵다. 특수화 함수라는 것을 표시하기 위해 template <>로 시작하며 어떤 타입에 대한 특수화인지 함수명 다음의 <> 괄호 안에 밝힌다. 앞쪽에 <>가 없으면 명시적 구체화 구문이 되므로 이 괄호를 생략해서는 안된다. 특수화 대상 타입은 인수의 목록으로 알 수 있어 생략 가능하며 <> 괄호까지 생략하는 것도 허용된다.
template <> void Swap(double &a, double &b)
template <> void Swap(double &a, double &b)
함수명 뒤쪽의 <double>이 없어도 인수의 타입을 통해 실수에 대한 특수화임을 알 수 있다. 단, 타입 인수가 함수의 인수로 사용되지 않고 리턴값이나 지역 변수로 사용될 때는 특수화 대상 타입을 생략할 수 없다.
클래스 템플릿에 대한 특수화 방법은 거의 비슷하다. 특정 타입에 대해 약간 다른 형태의 클래스를 만들고 싶다면 원하는 타입에 대해 다음 형식으로 특수화한다.
template<> class 클래스명 <특수타입>
다음 예제는 double x타입에 대해 PosValue 클래스를 특수하게 정의한 것이다.
// 파일이름 : Specializtion2.cpp
#include "cursor.h"
#include <iostream>
using namespace std;
template <typename T>
class PosValue
{
private:
int x, y;
T value;
public:
PosValue(int x, int y, T v) : x(x), y(y), value(v) { }
void outvalue()
{
gotoxy(x, y);
cout << value << endl;
}
};
template <> class PosValue<double>
{
private:
int x, y;
double value;
public:
PosValue(int x, int y, double v) : x(x), y(y), value(v) { }
void outvalue()
{
gotoxy(x, y);
cout << "[" << value << "]" << endl;
}
};
int main()
{
PosValue<int> iv(10, 10, 2);
PosValue<char> cv(25, 5, 'C');
PosValue<double> dv(30, 15, 3.14);
iv.outvalue();
cv.outvalue();
dv.outvalue();
}
특수화는 <>안에 아무것도 쓰지않은 것을 찾으면된다. 실수에 대해서는 값을 [] 괄호로 감싸 출력하도록 했다.
특수화된 클래스는 타입이 이미 결정되어 있어 타입 인수 T를 쓰지 않고 특수화된 타입을 클래스 정의문에 바로 사용한다. double 타입에 대해 특수화된 객체가 생성되며 출력 형태도 다르다.
템플릿 인수가 여러 개 있을때 그중 하나에 대해서만 특수화할 수도 있다. 이런 기법을 부분 특수화(Partial Specialization)라고 한다.
template <typename, T1, typename T2> class SomeClass { ... }
SomeClass 템플릿은 두 개의 인수를 가지며 <int, int>, <int, double>, <short, unsigned> 등 두 타입의 조합을 마음대로 선택할 수 있다. T1은 마음대로 선택하도록 내버려 두고 T2가 double인 경우에 대해서만 특수화하고 싶다면 다음과 같이 한다.
template <typename T1> class SomeClass<T1, double> { ... }
이 상태에서 SomeClass<int, unsigned>나 SomeClass<float, short>는 특수화하지 않은 버전의 템플릿으로부터 생성되지만 SomeClass<int, double>이나 SomeClass<char, double>은 부분 특수화된 템플릿으로부터 중첩된다.
템플릿 중첩
템플릿 클래스도 하나의 타입이다. 다만 완전한 타입으로 인정되려면 타입 인수를 항상 밝혀야 한다. PosValue는 템플릿의 이름일 뿐이며 PosValue<int> 나 PosValue<double>이 타입이다. 템플릿 클래스 타입을 함수로 전달하거나 리턴할 때도 일반 타입을 쓸 때처럼 인수열에 타입명을 밝혀야한다.
void sub(PosValue<int> pi)
이 함수는 PosValue<int> 타입의 객체 pi를 인수로 전달받는다. int나 double 타입을 인수로받는 것과 아무 차이가 없되 템플릿이 요구하는 타입 인수를 <> 괄호로 명시한다는 점만 다르다. 템플릿의 타입 인수 자리에는 타입이 들어가고 템플릿 클래스도 하나의 타입이다.
이 말은 템플릿끼리 중첩이 가능하다는 의미이다. 템플릿 선언문의 타입 인수에 또 다른 템플릿이 들어갈 수 있다.
아주 간단한 중첩의 예를 작성해보자.
// 파일이름 : NestTemplate.cpp
#include "cursor.h"
#include <iostream>
using namespace std;
template <typename T>
class PosValue
{
private:
int x, y;
T value;
public:
PosValue() : x(0), y(0), value(0) {}
PosValue(int x, int y, T v) : x(x), y(y), value(v) {}
void outvalue()
{
gotoxy(x, y);
cout << value << endl;
}
};
template <typename T>
class Wrapper
{
private:
T member;
public:
void set(T v) { member = v; }
T get() { return member; }
};
int main()
{
Wrapper<PosValue<char>>wrap;
wrap.set(PosValue<char>(10, 10, 'a'));
PosValue<char> pc = wrap.get();
pc.outvalue();
}
PosValue 템플릿은 지금까지 사용하던 것이되 빈 객체를 생성하기 위해 디폴트 생성자만 추가했다.
Wrapper 템플릿은 임의 타입의 값 하나를 래핑하는 템플릿이다. 여러 개의 값을 모으면 배열이나 스택이 되는데 복잡해지므로 그냥 단순한 래핑만 하며 값을 변경하거나 읽는 기능만 제공한다.
다음은 Wrapper로 정수와 실수는 감싸는 객체를 생성한다.
Wrapper<int> wi;
Wrapper<double> wd;
Wrapper의 타입 인수로 int를 주면 정수를 감싸고 double을 주면 실수를 감싼다. 그렇다면 int나 double 자리에 같은 자격을 가지는 타입인 PosValue<char>를 써주면 이 객체도 래핑할 수 있을까? 이렇게되면 템플릿끼리 중첩이 발생한다.
이런 중첩문을 작성할 때 다음과 같이 선언하면 안된다.
Wrapper <PosValue<char>> wrap;
중첩을 사용할때 템플릿의 타입 인수를 닫는 > 괄호가 두개가 왔는데, 이 괄호를 붙여서 >>로 쓰면 시프트 연산자로 해석되어 에러처리된다. 따라서 템플릿을 중첩할때는 두 괄호사이에 공백을 넣어 연산자가 아님을 분명히 해야한다.
하지만 비쥬얼 스튜디오에서는 이런 에러를 잡아줘서 띄워쓰든 붙여쓰든 에러가 뜨지는 않는다.
템플릿의 중첩 횟수에는 제한이 없어 이중, 삼중도 중첩가능하며 실전에서도 중첩이 사용된다고한다.
그러나 중첩이 가능하려면 두 클래스가 임의 타입에 대해 잘 작동하도록 충분히 일반화되어야하며 생성자, 대입 연산자 등 모든 장치가 제대로 마련되어있어야한다.
'개발자과정준비 > C++' 카테고리의 다른 글
[C++] 타입정보(typeid, static_cast, dynamic_cast, const_cast, 멤버 포인터 변수) (0) | 2021.07.06 |
---|---|
[C++] 예외(중첩예외, 예외클래스 계층, 예외캡슐화, 미처리 예외, 예외지정) (0) | 2021.07.05 |
[C++] 템플릿 - 1 (함수템플릿, 구체화, 명시적 인수, 임의 타입 지원 조건) (0) | 2021.06.10 |
[C++] 다형성(가상함수, 동적결합, 가상소멸자, 순수가상함수) (0) | 2021.06.09 |
[C++] 상속(멤버 함수 재정의, 다중상속, 클래스 재활용, 포함, 중첩클래스) (0) | 2021.06.08 |