[C++ Primer Plus] Chapter 14. C++ 코드의 재활용 (2) 템플릿
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
코드의 재활용성을 높이는 방법 #
public
상속- 컨테인먼트(containment) = 컴포지션(composition) = 레이어링(layering)
private
상속protected
상속- 클래스 템플릿 (이것에 대해 알아보겠다.)
클래스 템플릿 #
Stack
과Queue
를 구현한 클래스를 떠올려보자.- 저장되는 객체형이
double
일 수도 있고,string
일 수도 있다. - 그렇다면 바뀔 때마다 코드를 새로 작성할 것인가?
- 포괄적인(데이터형과 무관한) 방식으로 정의하면 좋지 않을까?
- 저장되는 객체형이
- 클래스 템플릿의 정의
- 템플릿들은 함수가 아니다. 그래서 개별적으로 컴파일할 수 없다.
- 따라서 모든 템플릿 관련 정보를 헤더 파일에 집어 넣는다.
// Stack.h
template <class Type> // 템플릿 클래스이다.
class Stack
{
private:
enum { MAX = 10 };
Type items[MAX];
int top;
public:
Stack();
bool IsEmpty();
bool IsFull();
bool Push(const Type& item);
bool Pop(Type& item);
};
template <class Type> // 템플릿 멤버 함수이다.
Stack<Type>::Stack() // 클래스 제한자가 Stack<Type> 이다. 인라인이면 생략가능하다.
{
top = 0;
}
template <class Type>
bool Stack<Type>::IsEmpty()
{
return top == 0;
}
template <class Type>
bool Stack<Type>::IsFull()
{
return top == MAX;
}
template <class Type>
bool Stack<Type>::Push(const Type& item)
{
if (top < MAX)
{
items[top++] = item;
return true;
}
return false;
}
template <class Type>
bool Stack<Type>::Pop(Type& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
return false;
}
- 템플릿 클래스의 사용
- 구체화를 요청해야 템플릿 클래스가 생성된다.
- 아래 예제에서는 두 개의 서로 다른 클래스 선언과 메서드들의 집합이 생성될 것이다.
Stack<int> iStack; // int 값들의 스택을 생성한다.
Stack<double> dStack; // double 값들의 스택을 생성한다.
- 일반적인 함수 템플릿과 다르게, 사용하려는 데이터형을 명시적으로 제공해야 한다.
Type
과 같은 포괄적인 데이터형 식별자를 데이터형 매개변수라고 한다.
// 일반적인 함수 템플릿
template <class T>
void Simple(T t) { cout << t << endl; }
Simple(2); // 컴파일러가 데이터형을 판단해서 결정한다.
Simple("two");
-
포인터들의
Stack
을 만드면 어떨까? -
string
데이터형을 사용하는 원본 코드
Stack<string> ss;
string po;
cin >> po;
- [시도 1]
po
를char *
로 바꿔본다.- 입력이 안 된다.
Stack<char *> sc;
char* po;
cin >> po; // (X) 저장 공간도 없는 곳에 저장할 수 없다.
- [시도 2]
po
를char
배열형으로 바꿔본다.- 입력이 된다.
- 하지만
Pop()
에서 불화를 일으킨다.
Stack<char *> sc;
char po[40];
cin >> po; // (O)
//...
sc.Pop(po);
template <class Type>
bool Stack<Type>::Pop(Type& item) // 배열이름을 참조한다.
{
if (top > 0)
{
item = items[--top]; // 배열이름에 대입할 수는 없다.
return true;
}
return false;
}
- [시도 3]
po
를char *
로 바꾸고 저장 공간을 대입한다.- 입력이 되고, 괜찮지만, 근본적인 문제가 발생한다.
po
가 늘 같은 메모리 위치를 지시하므로, 푸시를 여러번 해도 동일한 주소를 스택에 넣는다.
Stack<char *> sc;
char* po = new char[40];
cin >> po; // (O)
sc.Push(po);
cin >> po;
sc.Push(po); // 매번 같은 주소가 들어간다.
sc.Pop(po); // 매번 같은 주소가 나온다.
- 포인터들의 스택을 바르게 만드는 방법
char* po
대신 포인터들의 배열을 제공한다.
template <class Type>
class Stack
{
private:
enum { SIZE = 10 };
int stacksize;
Type* items; // 스택 항목들을 저장한다.
int top;
public:
explicit Stack(int ss = SIZE);
Stack(const Stack& st); // 복사 생성자를 제공한다.
~Stack() { delete[] items; } // 파괴자를 제공한다.
bool IsEmpty() { return top == 0; }
bool IsFull() { return top == stacksize; }
bool Push(const Type& item);
bool Pop(Type& item);
// 클래스 사용 범위 안에서는 Stack<Type>을 Stack으로 표현할 수 있다.
Stack& operator=(const Stack& st); // 대입 연산자를 제공한다.
};
template <class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0)
{
items = new Type[stacksize];
}
template <class Type>
Stack<Type>::Stack(const Stack& st)
{
stacksize = st.stacksize;
top = st.top;
items = new Type[stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
}
template <class Type>
bool Stack<Type>::Push(const Type& item)
{
if (top < stacksize)
{
items[top++] = item;
return true;
}
return false;
}
template <class Type>
bool Stack<Type>::Pop(Type& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
return false;
}
template <class Type>
Stack<Type>& Stack<Type>::operator=(const Stack<Type>& st)
{
if (this == &st) return *this;
delete[] items;
stacksize = st.stacksize;
top = st.top;
items = new Type[stacksize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
return *this;
}
int main()
{
Stack<const char*> st(5);
const char* inputs[10] = { "Bob", "John", "Amy", "Amanda", "Kal", "Lily", "Andy", "Harry", "Kim", "Ron"};
const char* outputs[10];
st.Push(inputs[0]);
st.Push(inputs[1]);
st.Push(inputs[2]);
st.Push(inputs[3]);
st.Pop(outputs[0]);
cout << outputs[0] << endl; // inputs[3] "Amanda"
st.Push(inputs[4]);
st.Pop(outputs[1]);
cout << outputs[1] << endl; // inputs[4] "Kal"
// ...
}
데이터형이 아닌, 수식 매개변수(expression argument) #
- 조건
- (1) 정수형
- (2) 열거형
- (3) 참조
- (4) 포인터
- 수식 매개변수의 값을 변경하거나 그것의 주소를 얻을 수 없다.
- 장점
- 생성자 접근 방식은
new
와delete
에 의해 관리되는 힙 메모리를 사용하지만, 수식 매개변수 접근 방식은 스택 메모리를 사용한다. 따라서 더 빠르다.
- 생성자 접근 방식은
- 단점
- 수식 매개변수가 다르면 자신만의 템플릿을 각각 생성한다.
- 생성자 접근 방식은
stacksize
를 멤버로 가지고 있기 때문에 좀 더 융통성이 있다. 예를 들면, 이것으로 다른 크기의 배열에 대입하거나, 크기를 조절할 수 있다.
- 장점
template <class T, int n> // int n은 수식 매개변수이다.
class ArrayTP
{
private:
T ar[n]; // 스택을 사용한다.
public:
ArrayTP() {};
explicit ArrayTP(const T& v);
virtual T& operator[](int i);
virtual T operator[](int i) const;
};
template <class T, int n>
ArrayTP<T, n>::ArrayTP(const T& v)
{
for (int i = 0; i < n; i++)
ar[i] = v;
}
template <class T, int n>
T& ArrayTP<T, n>::operator[](int i)
{
if (i < 0 || i >= n)
{
std::cerr << "배열의 경계를 벗어나는 에러: " << i
<< " --> 잘못된 인덱스입니다. \n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template <class T, int n>
T ArrayTP<T, n>::operator[](int i) const
{
if (i < 0 || i >= n)
{
std::cerr << "배열의 경계를 벗어나는 에러: " << i
<< " --> 잘못된 인덱스입니다. \n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
// 12, 13. 수식 매개변수가 다르므로 서로 다른 두개의 클래스 선언을 생성한다.
ArrayTP<double, 12> a1;
ArrayTP<double, 13> a2;
// 하나의 클래스 선언을 생성한다.
Stack<double> s1(12);
Stack<double> s2(13);
템플릿 클래스의 융통성 #
- (1) 기초 클래스의 역할을 할 수 있다.
- (2) 성분 클래스가 될 수도 있다.
- (3) 데이터형 매개변수가 될 수도 있다.
- (4) 템플릿을 재귀적으로 사용할 수 있다.
- (5) 하나 이상의 데이터형 매개변수를 사용할 수 있다.
- (6) 데이터형 매개변수에 디폴트 값을 제공할 수 있다.
template <class T>
class A
{
};
template <class T>
class B : public A<T> // 1. 템플릿 클래스 A를 상속한다.
{
};
template <class T>
class Other
{
A<T> a; // 2. 성분으로 사용될 수 있다.
};
int main()
{
A<Other<int>> a1; // 3. 데이터형 매개변수가 될 수 있다.
}
ArrayTP<ArrayTP<int, 5>, 10> ar; // 4. 템플릿을 재귀적으로 사용할 수 있다.
// int ar[10][5]; 와 동일하다.
template<class T1, class T2> // 5. 하나 이상의 데이터형 매개변수를 사용할 수 있다.
class Pair
{
private:
T1 a;
T2 b;
public:
Pair(T1 aVal, T2 bVal) : a(aVal), b(bVal) {}
};
int main()
{
Pair<string, int> p("이동갈비", 5);
Pair<string, int> p("태릉갈비", 5);
}
template<class T1 = string, class T2 = int> // 6. 데이터형 매개변수에 디폴트 값을 제공할 수 있다.
class Pair {};
클래스 템플릿 매개변수와는 다르게, 함수 템플릿 매개변수에는 디폴트 값을 제공할 수 없다. 하지만, 둘 다 데이터형이 아닌(수식) 매개변수에 대해서는 디폴트 값을 제공할 수 있다.
템플릿 클래스의 특수화 #
- (1) 암시적 구체화
- 지금까지 살펴 본 예제들과 같이, 객체를 선언하면 컴파일러가 하나의 특수화된 클래스 정의를 생성하는 것이다.
- 컴파일러는 객체가 요구될 때까지 그 클래스의 암시적 구체화를 생성하지 않는다.
ArrayTP<int, 10> ar; // 암시적 구체화
ArrayTP<int, 10> * pt; // 포인터, 아직 객체가 필요없다.
pt = new ArrayTP<int, 10> // 이제 객체가 요구된다.
- (2) 명시적 구체화
template
키워드를 사용하여 클래스를 선언하고,- 사용하려는 데이터형을 나타냈을 때 컴파일러는 명시적 구체화를 생성한다.
- 그 선언은 템플릿 정의와 동일한 이름 공간 안에 있어야 한다.
template class ArrayTP<int, 10>; // ArrayTP<int, 10> 클래스를 생성한다.
- (3) 명시적 특수화
- 포괄적인 데이터형 대신에 구체적인 하나의 데이터형에 맞게 정의된 템플릿 형식을 취한다.
template <class T>
class SortedArray
{
//...
};
// char *형을 위한 특수화된 템플릿을 제공한다.
template <> class SortedArray<char *>
{
//...
};
int main()
{
SortedArray<int> i; // 포괄적인 정의를 사용한다.
SortedArray<char *> c; // 특수화된 정의를 사용한다.
}
- 함수 vs 클래스
특징 | 함수 | 클래스 |
---|---|---|
암시적 구체화 | Swap(2, 3); |
ArrayTP<int, 10> a; |
명시적 구체화 | template void Swap<int>(int, int) |
template class ArrayTP<int 10> |
명시적 특수화 | template <> void Swap<int>(int, int) |
template <> class SortedArray<char *> |
- 부분적인 특수화
- 템플릿의 포괄성을 일부 제한한다.
// 포괄적인 템플릿
template <class T1, class T2>
class Pair
{
//...
};
// T2를 int로 설정한, 부분적인 특수화
template <class T1>
class Pair<T1, int>
{
//...
}
컴파일러는 가장 특수화된 템플릿을 사용한다.
멤버, 매개변수로써의 템플릿 클래스 #
- 템플릿은 구조체, 클래스, 템플릿 클래스의 멤버가 될 수 있다.
template <typename T>
class Beta
{
private:
template <typename V> // 내포된 템플릿 클래스 멤버
class hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const { cout << val << endl; }
V Value() const { return val; }
};
hold<T> q; // 템플릿 객체
hold<int> n;
public:
Beta(T t, int i) : q(t), n(i) {}
template<typename U> // 템플릿 메서드
U Blab(U u, T t)
{
return (n.Value() + q.Value()) * u / t;
}
void Show() const
{
q.show();
n.show();
}
};
int main()
{
// T가 3.5이므로 double로 설정된다.
Beta<double> guy(3.5, 3);
guy.Show();
// U가 10이므로 int가 된다. 그래서 리턴형도 int이다.
cout << guy.Blab(10, 2.3) << endl;
// U가 10.0이므로 double이 된다. 그래서 리턴형도 double이다.
cout << guy.Blab(10.0, 2.3) << endl;
}
- 매개변수 템플릿
- 데이터형, 수식말고도 템플릿 자체가 템플릿 매개변수로 들어갈 수 있다.
// template <typename T> class 데이터형인 Thing이 매개변수가 된다.
// 이전에 보았던 Stack이 그렇다.
template <template <typename T> class Thing>
class Crab
{
private:
Thing<int> s1; // Stack<int> s1;
Thing<double> s2; // Stack<double> s1;
public:
Crab() {};
bool push(int a, double x) { return s1.push(a) && s2.push(x); }
bool pop(int& a, double& x) { return s1.pop(a) && s2.pop(x); }
};
int main()
{
Crab<Stack> nebula;
int ni;
double nb;
cout << "int, double 쌍을 입력하세요." << endl;
while (cin >> ni >> nb && ni > 0 && nb > 0)
{
if (!nebula.push(ni, nb))
break;
}
while (nebula.pop(ni, nb))
cout << ni << ", " << nb << endl;
}
템플릿 클래스와 프렌드 함수 #
- 템플릿이 아닌 프렌드 함수
- 바운드 템플릿 프렌드 함수
- 언바운드 템플릿 프렌드 함수
1. 보통의 프렌드 함수를 템플릿 클래스 안에 넣기
template <typename T>
class HasFriend
{
private:
T item;
static int ct; // T에 따라서 각각의 특별한 특수화가 자신만의 static 멤버를 가질 것이다.
public:
HasFriend(const T& i) : item(i) { ct++; }
~HasFriend() { ct--; }
// friend 함수들
friend void Counts();
friend void Reports(HasFriend &);
// (X) HasFriend 객체 같은 건 없다.
// HasFriend<int>와 같은 특수화만이 존재할 뿐이다.
friend void Reports(HasFriend<T>&);
// Report 자체는 템플릿 함수가 아니다.
// 따라서 명시적 특수화를 정의해 주어야 한다.
};
// staic 멤버를 초기화해준다.
template <typename T>
int HasFriend<T>::ct = 0;
// friend 함수들
void Counts()
{
cout << "int: " << HasFriend<int>::ct << ", ";
cout << "double: " << HasFriend<double>::ct << endl;
}
void Reports(HasFriend<int>& hf)
{
cout << "HasFriend<int>: " << hf.item << endl;
}
void Reports(HasFriend<double>& hf)
{
cout << "HasFriend<double>: " << hf.item << endl;
}
int main()
{
Counts(); // int: 0, double: 0
HasFriend<int> hfi1(10);
Counts(); // int: 1, double: 0
HasFriend<int> hfi2(20);
Counts(); // int: 2, double: 0
HasFriend<double> hfdb(10.5);
Counts(); // int: 2, double: 1
Reports(hfi1); // 10
Reports(hfi2); // 20
Reports(hfdb); // 10.5
}
2. 클래스의 바깥에서 선언된 템플릿의 템플릿 특수화
-
바운드 템플릿 프렌드 함수
-
클래스가 구체화될 때 클래스의 데이터형에 의해 프렌드의 데이터형이 결정된다.
-
예를 들어,
int
클래스 특수화는,int
함수 특수화를 얻는 것이다. -
구현 방법
- (1) 클래스 정의 앞에 템플릿 함수를 선언한다.
- (2) 클래스의 템플릿 매개변수의 데이터형에 기초한 프렌드를 선언한다.
- (3) 프렌드의 정의를 제공한다.
// 1. 클래스 정의 앞에 템플릿 함수를 선언한다.
template <typename T> void Counts();
template <typename T> void Reports(T &);
template <typename TT>
class HasFriend
{
private:
TT item;
static int ct;
public:
HasFriend(const TT& i) : item(i) { ct++; }
~HasFriend() { ct--; }
// 2. 클래스의 템플릿 매개변수의 데이터형에 기초한 프렌드를 선언한다.
friend void Counts<TT>();
friend void Reports<>(HasFriend<TT> &);
};
template <typename T>
int HasFriend<T>::ct = 0;
// 3. 정의를 제공한다.
template <typename T>
void Counts()
{
cout << "카운트: " << HasFriend<T>::ct << endl;
}
template <typename T>
void Reports(T& hf)
{
cout << hf.item << endl;
}
int main()
{
Counts<int>(); // int: 0, double: 0
Counts<double>();
HasFriend<int> hfi1(10);
Counts<int>(); // int: 1, double: 0
Counts<double>();
HasFriend<int> hfi2(20);
Counts<int>(); // int: 2, double: 0
Counts<double>();
HasFriend<double> hfdb(10.5);
Counts<int>(); // int: 2, double: 1
Counts<double>();
Reports(hfi1); // 10
Reports(hfi2); // 20
Reports(hfdb); // 10.5
}
3. 클래스의 안에서 선언된 템플릿의 템플릿 특수화
- 언바운드 템플릿 프렌드 함수
- 바운드 템플릿 프렌드 함수와 다르게, 클래스의 데이터형과 다른 프렌드 템플릿 데이터형 매개변수를 갖는다.
- 모든 함수 특수화는 모든 클래스 특수화에 대해 프렌드이다.
template <typename T>
class ManyFriend
{
private:
T item;
public:
ManyFriend(const T& i) : item(i) {}
// 클래스의 데이터형 매개변수 T와 다른 C, D가 쓰였다.
template <typename C, typename D> friend void ShowTwo(C&, D&);
};
// Myfriend의 모든 특수화들에 대해 프렌드이기 때문에 이 함수는 모든 특수화들이 item멤버에 접근할 수 있다.
// 그러나 Myfriend<C>와 MyFriend<D> 객체들에 대한 접근만을 사용한다.
template <typename C, typename D> void ShowTwo(C& c, D& d)
{
cout << c.item << ", " << d.item << endl;
}
int main()
{
ManyFriend<int> hfi1(10);
ManyFriend<int> hfi2(20);
ManyFriend<double> hfdb(10.5);
ShowTwo(hfi1, hfi2); // 10, 20
ShowTwo(hfdb, hfi2); // 10.5, 20
}
템플릿 별칭 using = #
typedef
를 사용해서 별칭을 생성할 수 있다.- 하지만 이것은 특정 템플릿 부분에는 사용할 수 없다.
- 따라서
using
을 사용할 수 있다.
template<class T1, class T2>
class Test
{
};
typedef Test<int, int> tii; // (O)
template<class T>
typedef Test<T, int> tti; // (X) typedef는 템플릿에 사용 불가능하다.
template<class T>
using tti = Test<T, int>; // (O) using은 가능하다.
int main()
{
tti<double> test; // Test<double, int> 이다.
}