Skip to main content

[C++ Primer Plus] Chapter 14. C++ 코드의 재활용 (2) 템플릿

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




코드의 재활용성을 높이는 방법 #

  1. public 상속
  2. 컨테인먼트(containment) = 컴포지션(composition) = 레이어링(layering)
  3. private 상속
  4. protected 상속
  5. 클래스 템플릿 (이것에 대해 알아보겠다.)



클래스 템플릿 #

  • StackQueue를 구현한 클래스를 떠올려보자.
    • 저장되는 객체형이 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] pochar * 로 바꿔본다.
    • 입력이 안 된다.
Stack<char *> sc;

char* po;
cin >> po; // (X) 저장 공간도 없는 곳에 저장할 수 없다.  

  • [시도 2] pochar 배열형으로 바꿔본다.
    • 입력이 된다.
    • 하지만 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] pochar * 로 바꾸고 저장 공간을 대입한다.
    • 입력이 되고, 괜찮지만, 근본적인 문제가 발생한다.
    • 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) 포인터
  • 수식 매개변수의 값을 변경하거나 그것의 주소를 얻을 수 없다.
    • 장점
      • 생성자 접근 방식은 newdelete에 의해 관리되는 힙 메모리를 사용하지만, 수식 매개변수 접근 방식은 스택 메모리를 사용한다. 따라서 더 빠르다.
    • 단점
      • 수식 매개변수가 다르면 자신만의 템플릿을 각각 생성한다.
      • 생성자 접근 방식은 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. 템플릿이 아닌 프렌드 함수
  2. 바운드 템플릿 프렌드 함수
  3. 언바운드 템플릿 프렌드 함수

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> 이다. 
}