본문 바로가기

C++/C++ 기초 플러스 6판(Book)

[C++] 코드의 재활용(클래스 템플릿)

클래스 템플릿 정의

템플릿 클래스는 다음과 같은 형식을 코드를 앞세운다.

template <class Type>

 

여기 class를 사용하는 것은, Type이 반드시 클래스여야 하는 것을 의미하지 않는다.

이와 같은 상황에서 최신 C++ 컴파일러는 이처럼 혼동하기 쉬운 class 대신에 키워드 typename을 사용할 수 있다.

template <typename Type> // 최신 컴파일러

 

템플릿 클래스 자세히 들여다보기

이제 우리는 내장 데이터형이나 클래스 객체를 Stack<Type> 클래스 템플릿의 데이터형으로 사용할 수 있다. 그런데 포인터는 사용할 수 없을까? 예를 들어, string 객체 대신에 char형을 지시하는 포인터를 사용할 수 있을까? char형을 지시하는 포인터는 C++가 내부적으로 문자열을 처리하는 방법이다. 포인터들의 스택을 생성할 수는 있지만, 프로그램을 크게 수정하지 않는다면 그러한 스택은 바르게 동작하지 않을 것이다.

 

포인터들의 스택을 잘못 사용하는 경우

잘못된 3가지 예제가 모두 완벽하게 유효한 Stack<Type> 템플릿의 발효로 시작된다.

Stack<char *>st // char형을 지시하는 포인터들을 위한 스택을 생성한다.

 

그러고 나서 버전 1은 string po;과 같은 구문을 char * po; 같은 구문으로 대체한다.

이것은 string 객체 대신에 char형을 지시하는 포인터를 사용하여 키보드 입력을 받아들이겠다는 생각이다. 포인터의 단순한 생성은 입력 문자열들의 저장 공간을 생성하지 않기 때문에, 이와 같은 접근은 즉시 실패한다.

 

버전 2는 string po; 과 같은 구문을 char po[40];과 같은 구문으로 대체한다.

이것은 입력 문자열을 저장하기 위한 공간을 대입한다. 게다가 po는 char *형이므로 스택에 넣을 수 있다. 그러나 배열은 pop() 메서드에서 약속한 가정과 기본적으로 불화를 일으킨다.

template <class Type>
bool Stack<Type>::pop(Type & item)
{
    if(top > 0)
    {
        item = items[--tp];
        return true;
    }
    else
        return false;
}

 

첫째, 참조 변수 item은 배열 이름이 아니라 어떤 종류의 lvalue를 참조해야 한다.

둘째, 이 코드는 item에 대입할 수 있다고 가정한다. item이 어떤 배열을 참조할 수 있을지라도, 배열 이름에는 대입할 수 없다. 그러므로 이 접근도 실패한다.

 

버전 3은 string po; 과 같은 구문을 char * po = new char[40];과 같은 구문으로 대체한다.

이것은 입력 문자열을 저장하기 위한 공간을 대입한다. 게다가 po가 변수이므로 pop() 의 코드와 불화를 일으키지 않는다. 그러나 가장 근본적인 문제에 부닥친다. 즉, po 변수는 하나밖에 없고 그것은 항상 같은 메모리 위치를 지시한다. 그 메모리의 내욘은 새로운 문자열을 읽을 때마다 바뀐다. 그러나 푸시 연산은 매번 정확하게 동일한 주소를 스택에 넣는다. 그래서 스택을 팝하면, ㄷ항상 동일한 주소를 얻는다 그리고 그것은 메모리에 마지막으로 읽어드린 문자열을 항상 참조한다. 특히, 그와 같은 스택은 새 문자열이 들어올 때 마다 이들을 개별적으로 저장하지 않으므로, 유용한 용도로 사용할 수 없다.

 

포인터들의 스택을 바르게 사용하는 경우

포인터들의 스택을 사용하는 한 가지 방법은, 호출한 프로그램이 포인터들의 배열을 제공하는 것이다. 그 포인터들의 배열에서 각각의 포인터는 서로 다른 문자열을 지시한다. 이러한 포인터들을 스택에 넣는 것은, 각각의 포인터가 서로 다른 문자열을 참조하기 때문에 논리적으로 틀리지 않는다.

 

배열 템플릿 예제와 데이터형이 아닌 매개변수

배열의 크기를 수식 매개 변수로 결정하는 이와 같은 접근 방식은, Stack에서 사용한 생성자 접근 방식보다 하나의 장점을 가지고 있다. 생성자 접근 방식은 new와 delete에 의해 관리되는 힙(heap) 메모리를 사용하는 반면에, 수식 매개변수 접근 방식은 자동 변수들을 관리하는 스택 메모리를 사용한다. 특별히 이것은 크기가 작은 배열들을 많이 사용하는 경우에, 더 빠른 실행 속도를 제공한다.

 

수식 매개변수 접근 방식의 가장 큰 단점은, 각 배열 크기가 자신만의 템플릿을 각각 생성한다는 것이다. 즉, 다음과 같은 선언은

ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;

 

서로 다른 두 개의 클래스 선언을 생성한다. 그러나 다음과 같은 선언은

Stack<int> egges(12);
Stack<int> dunkers(13);

 

클래스 선언을 하나만 생성한다. 그리고 크기 정보는 그 클래스를 위한 생성자에 전달된다.

 

또 한 가지 차이는, 생성자 접근 방식은 배열 크기를 정의에 코드로 직접 적어 넣지 않고 클래스 멤버로 저장하기 때문에 좀더 융통성이 있다는 것이다. 예를 들면, 이것을 사용하여 한 크기의 배열로부터 다른 크기의 배열에 대입하는 것을 정의하거나, 크기를 조절할 수 있는 배열을 허용하는 클래스를 만들 수 있다.

 

클래스 템플릿 디폴트 데이터형 매개변수

클래스 템플릿이 가지고 있는 또 하나의 새로운 기능은, 데이터형 매개변수들에 디폴트 값을 제공할 수 있다는 것이다.

template <class T1, class T2 = int> class Topo{ ... };

 

이것은 컴파일러에게 T2를 위한 값이 생략되었을 때 T2에 해당하는 데이터형으로 int를 사용하라고 지시한다.

Topo<double, double> m1 // T1은 double, T2도 double
Topo<double> m2; // T1은 double, T2는 int

 

템플릿 특수화

클래스 템플릿은 암시적 구체화, 명시적 구체와, 명시적 특수화(이 세 가지를 합하여 특수화(specialization)라 한다.)를 사용할 수 있다는 점에서 함수 템플릿과 비슷하다. 즉, 템플릿은 클래스를 포괄형으로 서술하는 반면에, 특수화는 구체적인 하나의 데이터형을 사용하여 생성한 클래스 선언이다.

 

암시적 구체화

이 장에서 지금까지 살펴본 템플릿 예제들은 암시적 구체화(implicit instantiation)를 사용한다. 즉, 그 예제들은 사용하기를 원하는 데이터형을 나타내는 하나의 또는 그 이상의 객체를 선언한다. 그러면 컴파일러는 포괄적인 템플릿이 제공하는 설계도를 사용하여 하나의 특수화된 클래스 정의를 생성한다.

ArrayTP<int, 100> stuff; // 암시적 구체화

 

컴파일러는 객체가 요구될 때까지 그 클래스의 암시적 구체화를 생성하지 않는다.

ArrayTP<double, 30> * pt; // 포인터, 아직 객체가 필요 없다.
pt = new ArrayTP<double, 30>; // 이제 객체가 요구된다.

 

두 번째 구문은 컴파일러에게 하나의 클래스 정의와, 그 정의에 따라 생성되는 하나의 객체를 생성하라고 지사한다.

 

명시적 구체화

키워드 template을 사용하여 클래스를 선언하고, 사용하려는 데이터형을 나타냈을 때, 컴파일러는 명시적 구체화(explicit instantiation)를 생성한다. 그 선언은 템플릿 정의와 동일한 이름 공간 안에 있어야 한다. 예를 들면, 다음과 같은 선언은

template class ArrayTP<string, 100>; // ArrayTP<string, 100> 클래스를 생성한다.

 

ArrayTP<string, 100>이 클래스라고 선언한다. 이 경우에 컴파일러는, 그 클래스의 객체가 아직 생성되거나 언급되지 않았더라도, 메서드 정의들을 포함하여 그 클래스 정의를 생성한다. 암시적 구체화와 마찬가지로, 이 포괄적인 템플릿은 특수화를 생성하기 위한 설계도로 사용된다.

 

명시적 특수화

명시적 특수화(explicit specialization)는 포괄적인 템플릿 대신에, 사용하려는 특정한 데이터형(또는 데이터형들)을 위한 정의이다. 때로는 템플릿이 특정형에 맞게 구체화될 때 조금 다르게 행동하도록 수정해야 하는 경우가 있다. 이러한 경우에 우리는 명시적 특수화를 생성할 수 있다. 예를 들어, 정렬된(sorted) 배열을 나타내는 클래스를 위한 템플릿을 정의했다고 가정하자. 이때 항목들이 배열에 추가될 때 정렬이 이루어진다.

template <class T>
class SortedArray
{
    ... // 세부 사항은 생략
};

 

또한 그 템플릿이 값들을 비교하기 위해 > 연산자를 사용한다고 가정하자. 수치에 대해서는 이것이 잘 작동한다. T::operator>() 메서드가 정의되어 있을 때, T가 클래스형을 나타낸다면 이것은 잘 동작한다. 그러나 T가 char *형으로 나타내는 문자열이라면 이것은 동작하지 않는다.

 

실제로 템플릿은 동작한다. 그러나 문자열이 알파벳순이 아니라 그들이 저장되어 있는 주소를 기준으로 정렬된다. 그러므로 > 대신에 strcmp() 를 사용하는 클래스 정의가 필요하다. 그러한 경우에 우리는 명시적 템플릿 특수화를 제공할 수 있다.

 

이것은 포괄적인 데이터형 대신에 구체적인 하나의 데이터형에 맞게 정의된 템플릿 형식을 취한다. 특수화된 템플릿과 포괄적인 템플릿이 둘 다 구체화 요구에 적합한 상황에서 어느 하나를 선택해야 한다면, 컴파일러는 특수화된 버전을 사용한다.

 

특수화된 클래스 템플릿 정의는 다음과 같은 형식을 취한다.

template <> class Classname<specialized-type-name> { ... };

 

구형 컴파일러들은 template<> 가 생략된, 다음과 같은 형식만을 인식한다.

 class Classname<specialized-type-name> { ... };

 

새로운 표기법을 사용하여 char *형을 위한 특수화된 SortedArray 템플릿을 제공하려면, 다음과 같은 코드를 사용할 수 있다.

template <> class SortedArray<char *>
{
    ... // 세부 사항은 생략
};

 

이 구현 코드는 배열의 값들을 비교하기 위해 > 대신 strcmp()를 사용할 것이다. 이제 char *형을 위한 SortedArray 템플릿의 요청은, 포괄적인 템플릿 정의 대신에 특수화된 이 정의를 사용할 것이다.

SortedArray<int> scores; // 포괄적인 정의를 사용한다.
SortedArray<char *> dates; // 특수화된 정의를 사용한다.

 

부분적인 특수화

C++는 부분적인 특수화(partial specialization)도 허용한다. 부분적인 특수화는 템플릿의 포괄성을 일부 제한한다. 예를 들어, 부분적인 특수화는 데이터형 매개변수들 중의 어느 하나에 구체적인 데이터형을 제공할 수 있다.

// 포괄적인 템플릿
template <class T1, class T2> class Pair {...};

// T2를 int로 설정한, 부분적인 특수화
template <class T1> class Pair<T1, int> {...};

 

키워드 template 뒤에 있는 <>는 특수화되지 않은 데이터형 매개변수들을 선언한다. 따라서 두 번째의 템플릿 선언은, T2를 int로 특수화하지만 T1은 그대로 남겨 둔다. 데이터형을 모두 지정하면, 괄호 쌍은 비게 되고 완전한 명시적 특수화가 이루어진다.

// T1과 T2를 모두 int로 설정한, 완전한 명시적 특수화
template <> class Pair<int, int> {...};

 

여러 가지 중에서 하나를 선택해야 한다면, 컴파일러는 가장 툭수화된 템플릿을 사용한다.

Pair<double, double> p1 // 포괄적인 Pair 템플릿을 사용한다.
Pair<double, int> p2; // pair<T1, int> 부분적인 특수화를 사용한다.
Pair<int, int> p3; // pair<int, int> 완전한 명시적 특수화를 사용한다.

 

또는 포인터들을 위한 특별한 버전을 제공함으로써, 기존의 템플릿을 부분적으로 특수화시킬 수 있다.

template <class T> // 포괄적인 버전
class Feed {...};
template<class T*> // 포인터를 위한 부분적인 특수화
class Feeb{...} // 수정된 코드

 

포인터가 아닌 데이터형을 제공하면, 컴파일러는 포괄적인 버전을 사용한다. 포인터를 제공하면 포인터를 위한 부분적인 특수화 버전을 사용한다.

Feeb<char> fb1 // 포괄적인 템플릿을 사용한다, T는 char
Feeb<char *>fb2; // T* 특수화를 사용한다, T는 char

 

부분적인 특수화가 없으면, 두 번째 선언은 T를 char *형으로 해석하여 포괄적인 템플릿을 사용할 것이다. 부분적인 특수화가 있으면, T를 Char로 해석하여 특수화된 템플릿을 사용한다. 부분적인 특수화 기능은 다양한 제한을 만드는 것을 허용한다. 예를 들면, 다음과 같은 것들을 사용할 수 있다.

// 포괄적인 템플릿
template <class T1, class T2, class T3> class Trio {...};
// T3을 T2로 설정하는 특수화
template <class T1, class T2> class Trio <T1, T2, T3> {...};
// T3과 T2를 T1*로 설정하는 특수화
template <class T1> class Trio <T1, T2*, T3*> {...};

 

이와 같은 선언들이 주어지면, 컴파일러는 다음과 같이 선택한다.

Trio<int, short, char *>t1; // 포괄적인 템플릿을 사용한다.
Trio<int, short> t2; // Trio<T1, T2, T2>를 사용한다.
Trio<char, char *, char *> t3; // Trio<T1, T1*, T1*>를 사용한다.

 

매개변수 템플릿

지금까지 우리는, 템플릿이 typename T와 같은 데이터형 매개변수와, int n과 같은 데이터형이 아닌 매개변수를 가질 수 있다는 사실을 배웠다. 또한 템플릿은 그 자체가 템플릿인 매개변수를 가질 수 있다. 템플릿 매개변수는, 표준 템플릿 라이브러리를 구현하기 위해 사용된, 최근에 추가된 템플릿 기능이다.

template <template<typename T> class Thing>
class Crab

 

템플릿 매개변수는 template <typename T> class Thing이다. 여기서 template <typename T>class가 데이터형이고, Thing이 매개변수다. 이것은 무엇을 의미할까? 다음과 같은 선언이 있다고 가정하자.

Crab<King> legs;

 

이것이 허용되려면, 템플릿 매개변수 King이 템플릿 클래스여야 한다. 그리고 그 템플릿 클래스의 선언이 템플릿 매개변수 Thing의 선언과 일치해야 한다.

template <typename T>
class King {...};

 

Crab 선언은 다음과 같이 두 개의 객체를 선언한다.

template <template<typename T> class Thing>
class Crab
{
private:
    Thing<int> s1;
    Thing<double> s2;
...
}

 

따라서 앞서의 legs  선언은 Thing<int>를 King<int>로, Thing<double>은 King<double>로 대체할 것이다.

그래서 다음과 같은 선언을 하면

Crab<Stack> nebula;

 

Thing<int>는 Stack<int>로 구체화되고, Thing<double>은 Stack<double>로 구체화될 것이다. 다시 말해, 템플릿 매개변수 Thing은 Crab 객체의 선언에 템플릿 매개변수로 사용되는 템플릿형으로 대체된다.

 

템플릿 클래스와 프렌드 함수

템플릿 클래스 선언도 프렌드를 가질 수 있다. 템플릿의 프렌드를 다음 3가지로 분류할 수 있다.

  • 템플릿이 아닌 프렌드
  • 바운드 템플릿 플렌드, 클래스가 구체화될 때 클래스의 데이터형에 의해 프렌드의 데이터형이 결정된다.
  • 언바운드 템플릿 프렌드, 프렌드의 모든 특수화가 그 클래스의 각 특수화에 대해 프렌드들이다.

이제 각각의 예를 살펴보자

 

템플릿 클래스에 대한 템플릿이 아닌 프렌드 함수

템플릿 클래스에 있는 보통의 함수를 프렌드로 선언해 보자.

tempate <class T>
class HasFriend
{
    friend void counts(); // 모든 HasFriend 구체화들에 대해 프렌드이다.
    ...
};

 

이 선언은 counts() 함수를 그 템플릿의 가능한 모든 구체화들에 대해 프렌드로 만든다. 예를 들어, counts() 함수는 HasFriend<int> 클래스에 대해 프렌드 이고, HasFriend<string> 클래스에 대해서도 프렌드이다.

 

counts() 함수는 객체에 의해 호출되지 않는다. 그리고 그것은 객체 매개변수들을 가지지 않는다. 그렇다면 그것은 HasFriend 객체에 어떻게 접근할 수 있을까? 몇 가지 가능성이 있다. 

 

전역 객체에 접근할 수 있다.

전역 포인터를 사용하여 전역이 아닌 객체에 접근할 수 있다.

템플릿 클래스의 static 데이터 멤버에 접근할 수 있다. 그리고 그러한 static 데이터 멤버들은 객체와 따로 분리되어 존재한다. 어떤 프렌드 함수에 템플릿 클래스 매개변수를 게종하기를 원한다고 가정하자. 예를 들어, 다음과 같은 프렌드 선언이 과연 가능할까?

friend void report(HasFriend &); // 가능할까?

 

그것은 불가능하다. 왜냐하면 HasFriend 객체와 같은 것은 없기 때문이다. HasFriend<short>와 같은 특별한 특수화만이 존재한다. 따라서 템플릿 클래스 매개변수를 제공하려면, 하나의 특수화를 나타내야 한다. 예를 들면, 다음과 같은 것을 사용할 수 있다.

template <class T>
class HasFriend
{
    friend void report(HasFriend<T> &); //바운드 템플릿 프렌드
    ...
};

 

이것이 무엇을 하는지 이해하려면, 특별한 데이터형 객체를 선언할 때 만들어지는 특수화를 상상하라.

HasFriend<int> hf;

 

컴파일러는 템플릿 매개변수 T를 int로 대체할 것이다. 그것은 다음과 같은 형식의 프렌드 선언을 제공할 것이다.

class HasFriend<int>
{
    friend void report(HasFriend<int &); // 바운드 템플릿 프렌드
    ...
};

 

즉, HasFriend<int> 매개변수를 사용하는 report()는, HasFriend<int> 클래스에 대해 프렌드가 된다. 같은 논리로, HasFriend<double> 매개변수를 사용하는 report()는, HasFriend<double> 클래스에 대한 프렌드인, 오버로딩 버전의 report() 가 될 수 있다. report() 자체는 템플릿 함수가 아니다. 단지 그것은 템플릿을 매개변수로 가진다. 이것은 사용하려는 프렌드들에 대해서 명시적 특수화를 정의해 주어야 한다는 것을 의미한다.

void report(HasFriend<short> &) {...}; // short을 위한 명시적 특수화
void report(HasFriend<int> &) {...}; // int를 위한 명시적 특수화

 

HasFriend 템플릿은 ct라는 static 멤버를 하나 가진다. 이것은 그 클래스의 각 특별한 특수화가 그 자신만의 static 멤버를 가진다는 것을 의미한다. HasFriend의 모든 특수화에 대한 프랜드인 counts() 메서드는, HasFriend<int>와 HasFriend<double>이라는 두 특별한 특수화에 대해 각각 ct의값을 보고한다. 또한 프로그램은 두 개의 report() 함수를 제공한다. 그들은 각각의 특별한 HasFriend 특수화에 대해 프렌드이다.

 

템플릿 클래스에 대한 바운드 템플릿 프렌드 함수

프렌드 함수들은 템플릿 자체로 만들 수 있다. 특별히 우리는 그것들을 바운드 템플릿 프렌드로 설정할 수 있다. 그래서 클래스의 각 특수화는 하나의 프렌드에 일치하는 특수화를 얻는다. 이것은 템플릿이 아닌 프렌드보다 다소 복잡하며, 세 단계를 거친다.

 

제 1단계로, 클래스 정의 앞에 템플릿 함수를 각각 선언하라.

template <typename T> void counts();
template <typename T> void report(T &);

 

제2 단계로, 그 함수 안에서 다시 템플릿들을 프렌드로 선언하라. 이 구문들은 클래스 템플릿 매개 변수의 데이터형에 기초하는 특수화들을 선언한다.

template <typename TT>
class HasFriendT
{
    ...
    friend void counts<TT>();
    friend void report<>(HasFriendT<TT> &);
};

 

선언에 있는 <>에 의해 이들은 템플릿 특수화로 인식된다. report()의 경우에 함수 매개변수로부터 템플릿 데이터형 매개변수

HasFriendT<int> squack;

 

이제 컴파일러는 TT를 int로 대체한다. 그러고 나서 다음과 같은 클래스 정의를 생성한다.

class HasFriendT<int>
{
...
    friend void counts<int>();
    friend void report<>(HasFriendT<int> &);
};

 

하나의 특수화는 int로 대체되는 TT에 기초를 두고 있다. 다른 하나의 특수화는 HasFriendT<int>로 대체되는 HasFriendT<TT>에 기초를 두고 있다. 그래서 그 템플릿 특수화들은 counts<int>()와 report<HasFriendT<int>>()를 HasFriendT<int> 클래스에 대해 프렌드로 선언한다.

 

제3 단계로, 프로그램은 그 프렌드들을 위한 정의를 제공해야 한다. HasFriend 클래스들이 대해 프렌드인 counts() 함수를 하나만 가지고 있다. 그러나 구체화된 클래서형의 각각에 대해 하나씩 두 개의 counts() 함수를 가진다.

 

템플릿 클래스에 대한 언바운드 템플릿 프렌드 함수

앞의 섹션에서 제시된 바운드 템플릿 프렌드 함수들은, 클래스의 바깥에서 선언된 템플릿의 템플릿 특수화들이다. 예를 들어, int 클래스 특수화는 int 함수 특수화를 얻는 형식이다. 템플릿은 클래스 안에 선언함으로써, 언바운드 프렌드 함수를 생성할 수 있다. 이때 모든 함수 특수화는 모든 클래스 특수화에 대해 프렌드이다. 언바운드 프렌드들의 경우에, 프렌드 템플릿 데이터형 매개변수들은 템플릿 클래스 데이터형 매개변수들과 다르다.

template<typename T>
class ManyFriend
{
...
    template<typename C, typename D> friend void show2(C &, D &);
};

 

프로그램에서 show2(hfi1, hfi2) 함수 호출은 다음과 같은 특수화의 일치한다.

void show2<ManyFriend<int> &, ManyFriend<int> &>(ManyFriend<int> & c, ManyFriend<int> & d);

 

이것은 ManyFriend의 모든 특수화들에 대해 프렌드이기 때문에, 이 함수는 모든 특수화들의 item 멤버에 접근할 수 있다. 그러나 ManyFriend<int> 객체들에 대한 접근만을 사용한다.

 

같은 논리로, show2(hfdb, hfi2)는 다음과 같은 특수화와 일치한다.

마찬가지로 바로위 코드도 ManyFriend의 모든 특수화들에 대한 프렌드이다. 이 함수는 ManyFriend<int> 객체의 item 멤버와, ManyFriend<double> 객체의 item 멤버에 대한 접근을 사용한다.