Computer Science/C & C++

[C++] Class Template | 클래스 템플릿

lww7438 2020. 3. 12. 17:55

Class Template

클래스 템플릿

- 함수 템플릿에서 확장된 개념으로, C++의 \(\texttt{class}\)에도 템플릿을 적용할 수 있다.
(기본적인 \(\texttt{template}\) Syntax는 윗 줄의 함수 템플릿 링크를 참조)
- 템플릿 클래스 선언과 템플릿 메서드 정의는 근본적으로 클래스 정의와 메서드의 정의가 아니다. 클래스 템플릿은 그 자체로 하나의 틀을 제공하는 도구이며, 특정 데이터형이 배정되어 Instantiation(구체화)가 이루어져야 비로소 하나의 클래스 정의와 멤버 함수의 정의가 생성되는 것이다.

※ \(\texttt{export}\) 키워드 : 클래스 템플릿과 템플릿 멤버 함수들은 일반 클래스를 정의할 때와 달리, 분할 컴파일(별개의 파일로 분리하는 작업)이 기본적으로 불가능한데, 이를 가능하게 하는 키워드이다. C++11부터는 더이상 지원하지 않는 키워드이지만, 미래에 사용될 가능성을 고려하여 \(\texttt{export}\) 키워드는 예약어로 분류되어 있다.


Syntax (문법)

- 함수 템플릿과 마찬가지로, 템플릿 클래스는 아래 코드와 같은 \(\texttt{template}\) 제한자를 클래스 선언 앞에 붙여야 한다.

template <typename T>      // template <class T> 또한 가능!
class Stack {
    ...
};

※ \(\texttt{typename}\) 키워드 대신, 익숙한 \(\texttt{class}\) 키워드를 사용해도 문제는 없지만, 클래스 키워드인 \(\texttt{class}\)와 혼선이 생길 수 있어, \(\texttt{typename}\) 표기가 권고된다.

- \(\texttt{template}\) 메서드 정의 시에는, 템플릿 데이터형을 표시하는 홑화살괄호(<>)를 클래스 제한자 앞에 붙여야한다.

template <typename T>
bool Stack<T>::push(const T& item) {
    ...
}

※ 클래스 선언 안에서 메서드를 정의하면, \(\texttt{template}\) 제한자와 \(\texttt{class}\) 제한자를 생략할 수 있으며, 클래스 선언 안에서 정의가 선언된 메서드는 인라인 함수로 간주된다.
※ \(\texttt{T}\)와 같은 포괄적인 데이터형 식별자를 Type Parameter(데이터형 매개변수)라고 한다. 데이터형 매개변수에는 수치값이 아닌, 데이터형 자체가 대입된다.

- 템플릿 클래스 Instantiation & 객체 생성

// 템플릿 클래스 객체 선언의 일반형
className<dataType> objectName;

// 실 사용 예시
Stack<int> kernels;     // int형 클래스가 구체화되고 kernels 객체가 생성된다.


- 템플릿 선언 내에서의 약식 표기와 템플릿 선언 외부에서의 정식 표기는 \(\texttt{<typeParameter>}\) 의 유무의 차이이다.

template <typename Type>
class Stack {
    ...
    Stack& operator=(const Stack& st);       // 템플릿 내부에서의 약식 표기; <Type> 없음
    ...
};

Stack<Type>& Stack<Type>::operator=(const Stack<Type>& st) {    // 템플릿 외부에서의 정식 표기; <Type> 있음
    ...
}


- Default Type Parameter(디폴트 데이터형 매개변수) : 템플릿 매개변수에도 디폴트 값을 지정할 수 있다.

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

Topo<double, double> m1;    // T1은 double형, T2도 double형으로 지정된다.
Topo<double> m2;            // T1은 double형, T2는 디폴트 값(int형)으로 지정된다.

 ※ 이는 클래스 템플릿에만 한정된 특징이며, 함수 템플릿 매개변수에는 디폴트 값을 지정할 수 없다.
단, 클래스 템플릿과 함수 템플릿 모두 수식 매개변수에 대해서는 디폴트 값을 지정할 수 있다.


Expression Argument (수식 매개변수)

- 템플릿 정의에서는 Type Parameter(데이터형 매개변수)가 아닌, Expression Argument(수식 매개변수) 또한 수용할 수 있다

// 수식 매개변수(Expression Argument, Non-Type Argument)를 사용하는 템플릿 예시

template <class T, int n>
class ArrayTP {
private:
    T ar[n];
    ...
};

정수형, 열거형, 참조, 포인터가 수식 매개변수가 될 수 있다
ex) 수식 매개변수로써, \(\texttt{double n}\)은 허용되지 않지만, \(\texttt{double& n}\)은 가능하다.
- 템플릿 코드에서는 수식 매개변수의 값을 변경하거나 주솟값을 얻는 작업등은 할 수 없다.
ex) 수식 매개변수 \(\texttt{int n}\)이 있을 때, \(\texttt{n++}\)나 \(\texttt{&n}\)과 같은 표현은 허용되지 않는다.
- 템플릿이 구체화되어, 객체를 생성할 때, 수식 매개변수에 해당하는 값으로는 꼭 상수 수식이어야 한다. 수식 매개변수로 변수는 허용되지 않는다.
- 수식 매개변수를 통해, 사용자로부터 배열의 크기를 전달받는 경우, 수식 매개변수를 통해 사이즈가 정해진 배열은 자동 변수들과 같이 스택 메모리에 저장되기 때문에 크기가 작은 배열을 많이 사용하는 경우에선, \(\texttt{new}\)를 통해 Heap 영역에 배치되는 것보다 실행 속도 측면에서 유리하다.

- 템플릿 클래스는 하나의 데이터형에 대해 구체화가 요구되면, 그 데이터형에 맞춰진 클래스 정의가 하나 탄생하게 된다. 여기에서 확장된 개념으로, 수식 매개변수에 하나의 값이 주어져서 그에 대한 구체화가 요구되면, 그 값에 맞춰진 클래스 정의가 하나 탄생하게 된다.
즉, \(\texttt{int n}\)이라는 수식 매개변수가 있을 때, \(\texttt{n}\) 값에 1이 전달되면 \(\texttt{n = 1}\)인 경우의 구체화가 이루어지고, \(\texttt{n}\) 값에 2가 전달되면, \(\texttt{n = 2}\) 인 경우의 구체화가 이루어지는 비효율적인 상황이 발생하게 된다.

template <class T, int n>
class ArrayTP {
    ...
};

...

ArrayTP<double, 12> eggweights;    <double, 12>에 맞는 클래스 정의가 생성된다.
ArrayTP<double, 13> donuts;        <double, 13>에 맞는 클래스 정의가 생성된다.


// 그러나, 아래와 같은 생성자 접근 방식의 선언은 한 클래스 정의만 생성되어 효율적이다.
ArrayTP<double> eggweights(12);
ArrayTP<double> donuts(13);
// 물론, 위 클래스 정의와는 다른 구조로 뒷받침되어야 할 것이다.

※ 생성자 접근 방식의 다른 장점으로는, 배열 크기를 멤버로 저장함으로싸, 한 배열을 다른 크기의 배열에 대입하는 것을 가능하게 정의하거나 배열의 크기를 실행시간동안 조절할 수 있는 기능들을 구현할 수 있게한다는 것이다.


Inheritance for Template Class (템플릿 클래스의 상속)

- 템플릿 클래스 또한, 일반적인 클래스와 다를 바 없이 C++ 상속 기능을 적용시킬 수 있다.

// Ex. 템플릿 클래스의 상속 예시

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

template <typename Type>
class Son : public Father<type> {    // 상속
    ...
};

template <typename Tp>
class Daughter {
    Father<Tp> ar;    // 컨테인먼트
    ...
};


- 템플릿 매개변수로, 다른 템플릿 클래스가 올 때, \(\texttt{<<}\) 연산자와의 혼동을 피하기 위해 같은 기호의 홑화살괄호(<>) 간에 하나의 공백을 주는 것을 C++98에서는 문법으로 지정했었는데, C++11부터는 이러한 문법 사항을 제거했다.

// >> 연산자와의 혼선 예방 예시 (C++11부터는 상관없음)

Father < Son<int> > asi;   // 홑화살괄호 사이에 하나의 WhiteSpace가 존재한다. (C++98의 요구사항)
Father<Son<int>> asi;      // C++11부터는 신경 쓸 필요 없다.

Using a Template Recursively (템플릿의 재귀)

- 템플릿에도 재귀를 사용할 수 있다.

// Ex. 템플릿의 재귀적 사용

template <typename T, int n>
class ArrayTP {     // ArrayTP는 배열을 관리하는 사용자 정의 클래스라 가정하자.
    ...
};

ArrayTP< ArrayTP<int, 5>, 10> twodee;
// 위 구문은 아래 구문처럼 동작한다. (ArrayTP가 배열을 관리하는 클래스라는 가정 하에)
int twodee[10][5];

Template Specialization (템플릿 특수화)

- Specialization(특수화) = Implicit Instantiation(암시적 구체화) + Explicit Instantiation(명시적 구체화) + Explicit Specialization(명시적 특수화)
- Instantiation(구체화) & Specialization(특수화)에 관한 기초적인 개념은 이전 포스트를 참조하자.

- 클래스 템플릿에서도 명시적 구체화를 수행할 수 있다.

// Ex. 템플릿의 명시적 구체화 예시

template <typename T, int n>
class ArrayTP {
    ...
};


tamplate class ArrayTP<std::string, 100>;      // 명시적 구체화
// ArrayTP<std::string, 100> 클래스 정의를 생성한다.
// 해당 클래스의 객체가 생성되지 않았더라도, 클래스 정의를 먼저 만들어놓는다.


- Specialized Template Class(특수화 템플릿 클래스)는 특정 데이터형이나 특정 매개변수 값에 특수화된 클래스 템플릿 정의를 아래와 같은 Syntax로 선언할 수 있다.

// Ex. 특수화된 클래스 템플릿 정의의 일반형

template <> class className<specializedTypeName> {
    ...
};


// 실 사용 예시 (ArrayTP 템플릿 클래스가 기정의되어있다 가정하자.)

template <> class ArrayTP<char*> {
    ...
};


- Partial Specialization(부분적 특수화)는 특수화 템플릿 클래스에서 다수의 템플릿 매개변수 중 몇몇 템플릿 매개변수는 특수화 값을 지정하지 않고, 그대로 놔두는 형식의 특수화 방식이다.

// 포괄 템플릿, 부분적 특수화, 완전 특수화 예시


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


// 부분적 명시적 특수화 
// (T1은 템플릿 매개변수로 기존과 같이 모든 데이터형을 받아들이고, T2에 한해서 T2가 int일 경우 우선시 되는 특수화 클래스를 정의함)
template <class T1> class ArrayTP<T1, int> {
    ...
};


// 완전 명시적 특수화 (홑화살괄호 내부가 완전히 비워져있음)
template <> class ArrayTP<char*, int> {
    ...
};

- 포인터 매개변수들을 위한 특별한 버전의 템플릿 클래스를 제공하여 포인터 변수들을 위한 명시적 특수화를 정의할 수도 있다.

// Ex. 포인터들을 위한 명시적 특수화 템플릿 클래스 예시


// 포괄적인 템플릿 클래스
template <class T>
class ArrayTP {
    ...
};


// 포인터 템플릿 매개변수들을 위한 명시적 특수화 버전
template <class T*>
class ArrayTP {
    ...
};

...

ArrayTP<char> Ar1;    // 포괄적인 템플릿 클래스를 이용한다.
ArrayTP<char*> Ar2;   // T* 특수화 템플릿을 이용한다.


- 부분적 특수화를 응용해서, 다양한 경우의 매개변수 입력에 대응할 수 있다.

// 부분적 특수화 응용 예시


// 일반적인 포괄적 템플릿
template <class T1, class T2, class T3>
class Trio { ... };


// T3를 T2로 설정하는 부분적 특수화
template <class T1, class T2> class Trio<T1, T2, T2> { ... };


// T3와 T2를 T1*로 설정하는 부분적 특수화
template <class T1> class Trio<T1, T1*, T1*> { ... };

Member Template (멤버 템플릿)

- 템플릿은 구조체, 클래스, 템플릿 클래스의 멤버가 될 수 있다. (컨테인먼트가 가능하다.)

// Ex. 멤버 템플릿 예시

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(); }
};

Template As Parameters (매개변수로서의 템플릿)

- 템플릿은 그 자체가 템플릿인 매개변수를 가질 수 있다.

// Ex. 템플릿 매개변수 예시

// template<typename T> class : 템플릿 매개변수의 데이터형 부분
// Thing : 템플릿 매개변수의 이름
template <template<typename T> class Thing>  
class ArrayTP {
private:
    Thing<int> s1;
    Thing<double> s2;
    ...
};
// Ex. 템플릿 매개변수의 또 다른 예시

template <template <typename T> class Thing, typename U, typename V>
class Crab {
private:
    Thing<U> s1;
    Thing<V> s2;
    ...
};

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

...

Crab<Stack, int, double> nebula;    // T = Stack, U = int, V = double 로 대체된다.

Template Class and Friend (템플릿 클래스와 프렌드 함수)

- 템플릿 클래스도 프렌드 함수를 Grant 할 수 있으며, 템플릿의 프렌드를 아래 3가지로 분류할 수 있다.

1. Non-template friend(템플릿이 아닌 프렌드)

// Ex. 템플릿이 아닌 프렌드 예시

template <class T>
class HasFriend {
    friend void counts();    // 모든 HasFriend 구체화들에 대해 프렌드이다.
    ...                      // 즉, T가 어떤 데이터형이든 상관없다.
}

// 여기서, counts()는 멤버가 아니므로 객체에 의해 호출되지 않는다.
// 따라서, counts()는 일반 객체에는 접근하지 못하고, 전역 객체에 접근 가능하며,
// 전역 포인터를 이용해서 전역이 아닌 일반 객체에 접근할 수 있다.
// 또한, 클래스의 static 멤버에도 접근할 수 있다.

- 또한 클래스 객체를 매개변수로 지정할 때, 반드시 템플릿 데이터형도 명시되어야 한다.

// 매개변수로서의 템플릿 클래스 객체 주의사항 예시

friend void report(HasFriend&);
// 위 구문은 불가능한 구문이다.
// HasFriend<short>& 와 같이, 데이터형이 지정되어 있어야 한다.
// 템플릿 클래스 매개변수를 제공하려면, 하나의 특수화를 나타내어야 함을 의미한다.


// 올바른 구현 예시

template <class T>
class HasFriend {
    friend void report(HasFriend<T>&);
    ...
};


2. Bound template friend(바운드 템플릿 프렌드) : 클래스가 구체화될 때 클래스의 데이터형에 의해 프렌드의 데이터형이 결정되는 형태

// Ex. 바운드 템플릿 프렌드 함수 예시

template <typename T>
class HasFriend {
    friend void report(HasFriend<T>&);    // 이 프렌드 함수는 short형과 int형에 대비되었다.
    ...
};

void report(HasFriend<short>& parameters) {...};  // short형에 대비한 명시적 특수화

void report(HasFriend<int>& parameters) {...};    // int형에 대비한 명시적 특수화


// 위에서 보다시피, 명시적 특수화된 외부의 프렌드 함수 곁에는 template 제한자가 없음에 유의하자.

※ 명시적 특수화가 정의된 외부 프렌드 함수에는 \(\texttt{template}\) 제한자가 붙지 않음에 유의하자.
(왜냐하면, 그 함수들은 본질적으로 템플릿이 아니기 때문이다.)

- 프렌드 함수들을 템플릿으로 만드는 방법

// Ex. 프렌드 함수들을 템플릿으로 만드는 방법

// 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>>(HasFriendT<TT>&);
    // report()의 경우, report의 매개변수 HasFriendT<TT>&로 부터 템플릿 데이터형 매개변수를 유추할 수 있기 때문에,
    // friend void report<>(HasFriendT<TT>&); 처럼 홑화살괄호 내부 내용을 생략하여 표기할 수도 있다.
    // counts()함수는 매개변수가 없기 때문에 홑화살괄호 내부의 TT를 생략하면 안되는 것이다.
};

// 3단계. 프렌드 정의를 제공한다.
template <typename T>
void counts() { ... }

template <typename T>
void report(T& hf) { ... }



3. Unbound template friend(언바운드 템플릿 프렌드) : 프렌드의 모든 특수화가 그 클래스의 각 특수화에 대해 프렌드들인 형태

- 템플릿을 클래스 안에 선언함으로써, 언바운드 프렌드 함수를 생성할 수 있다.
(바운드 프렌드 함수는 엄연히, 클래스 밖에 선언된다.)
- 모든 함수 특수화는 모든 클래스 특수화에 대해 프렌드이다.

// 언바운드 프렌드 함수 정의 및 사용 예시

template <typename T>
class ManyFriend {
private:
    T item;
public:
    ManyFriend(const T& i) : item(i) { }
    
    template <typename C, typename D>    // 템플릿 함수가 클래스 내부에서 선언됨
    friend void show2(C&, D&);
};

template <typename C, typename D>
void show2(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);
    
    show2(hfi1, hfi2);
    show2(hfdb, hf1);
    
    return 0;
}