프로그래밍 지식/C++

C++문법/ (클래스) 템플릿, Template - 2

게임이 더 좋아 2021. 12. 16. 15:04
반응형
728x170

앞에서 함수 템플릿을 배웠듯이

클래스 템플릿도 뭔가 찍어내는 것 같은 느낌이다.

맞다. 하지만 조금 느낌은 다른데

 

클래스 템플릿은 구조나 알고리즘은 같되 멤버의 타입이 다른 클래스를 찍어내는 틀이다.

 

예를 보면서 알아보자

아래 클래스들은 화면의 특정 위치에 값 하나를 출력하는 클래스다.

 

우선 int, char 순서대로 

class PosValueInt
{
private:
    int x,y;
    int value;
    
public:
     PosValue(int ax, int ay, int av): x(ax), y(ay), value(av) {}
     void outvalue();
};

class PosValueChar
{
private:
    int x,y;
    char value;
public:
    PosValue(int ax, int ay, char av) : x(ax),y(ay),value(av) {}
    void outvalue();
};

...

 

출력 좌표를 지정하는 x,y는 모두 정수형으로 같지만

출력값인 value는 대상값의 종류에 따라 타입이 다르게 만들었다.

**타입만 다르다.

 

value의 타입이 제각각이라

이 값을 초기화하는 생성자의 원형도 다르다.

만약 오버로딩이 된다면 좋겠지만..?

클래스는 오버로딩이 지원되지 않아서 이름을 하나하나 붙여야 한다.

실제로 다른 부분은 value와 관련된 부분밖에 없고 나머지는 동일해서

템플릿으로 하는 것이 더욱 효율적이다.

 

즉, 위 클래스는 어떻게 바꾸어야 하느냐??

template <typename T>
class PosValue
{
private:
    int x,y;
    T value;
public:
    PosValue(int ax, int ay, T av) : x(ax), y(ay), value(av) {}
    void outvalue(){
        gotoxy(x,y);
        cout << value << endl;
    }
};

 

value만 타입이 다르길래

value를 템플릿으로 바꾸어주었다.

앞에 "명시적 선언"을 해준다면 해당 클래스는 해당 객체로 바뀌게 될 것이다.

PosValue<int> , PosValue<char>가 될 수 있겠다.

 

즉, 템플릿 클래스는 언제나 < >괄호가 따라다니면서 타입을 같이 밝혀서 선언한다.(default 제외)

다시 말해서 객체를 선언할 때는 괄호와 타입까지 반드시 밝혀야 한다.

해당 클래스에 대한 객체를 만들 때는 괄호와 타입을 명시적 선언을 해줘야 한다는 말이다.

 

예를 들어 Vector 컨테이너 선언을 어떻게 하더라?? 

vector <int> vec;

 

어??? 설마 vector 클래스자체도 템플릿인가??? 라고 생각했으면 정답이다.

모든 자료형에 대해 벡터 클래스를 정의했다면 정말 코드의 양이 많아질 뻔했다.

 

**생성자는 원래 클래스 이름을 따르지만 클래스 템플릿의 경우에는 <T>를 붙일 필요는 없고

템플릿 이름을 그대로 사용하면 된다.

 

**또한 템플릿 클래스도 일반 클래스와 같이 다뤄진다. 또한 일반클래스를 상속하는 것도 가능하다.

 

예를 들면 PosValue<int>로부터 새로운 파생 클래스를 만들 수 있다.

class PosValue2 : public PosValue<int>{ .. }

상속시키는 것도 가능하다는 이야기다.

 

하지만 기반 클래스로 사용된 클래스는 해당 타입의 객체를 생성하지 않더라도 바로 구체화된다.

-> 결정된다는 말이다. 템플릿 클래스가 타입이 결정되어 구체화됨을 의미한다.

왜냐하면 부모가 결정되어야 자식클래스가 정의되기 때문이다.

 


 

템플릿 클래스 뿐만이 아니라

템플릿으로 멤버함수도 선언할 수 있다.

일반 클래스 안에서도 멤버함수를 템플릿으로 선언할 수 있다는 것이다.

함수 템플릿과 똑같이 만들면 된다.

 

class Util
{
public:
    template<typename T>
    void swap(T &a, T &b)
    {
        T t;
        t=a; a=b; b=t;
    }
};

 

함수 템플릿과 다른 것이 없다.

템플릿 선언 -> 인수에 템플릿 사용 -> 끝~

 

위의 Swap은 해당 객체의 멤버함수로 여러 타입의 데이터에 대해서 Swap을 하나보구나 생각한다.

이렇게 되면 타입이 늘어날수록 해당 멤버는 함수가 1개였지만 점점 늘어나게 되겠다.

**하지만 다행히 멤버 함수의 개수는 객체의 크기에 영향을 주지는 않는다.

 

 


 

템플릿을 쓰기 전에 주의해야 할 점이 또 있다.

템플릿과 같은 명칭은 사용하기 전에 미리 정의되어 있어야 한다.

다시 말해서 순서가 바뀌면 템플릿의 정의를 알 수 없을 때도 있다.

 

우리가 예를 들어 볼때는 항상 템플릿을 정의하는 코드와 사용하는 코드를 같이 작성하지만

실제로는 클래스별로 모듈화해서 구성하는 것이 일반적이다.

 

템플릿도 별도의 모듈로 작성하게 되는데

즉, 템플릿 선언문과 멤버 함수의 정의까지 모두 헤더 파일에 작성하게 된다.

 

이렇게 되면 템플릿에 속한 멤버 함수를 내부에 인라인으로 선언할 때는 T에 대한 설명이 클래스 선언문 앞에 이미 있으니까 그냥 작성하면 된다.

다만 클래스 내부에선 선언만 하고 외부에서 정의할 때템플릿에 속한 멤버 함수임을 밝히기 위해서 클래스 이름을 붙여야 한다.

해당 클래스의 이름에 T가 포함되므로 template<typename T>가 먼저와야 한다.

다시말해서 PosValue 클래스 템플릿은 PosValue.h에 선언하고

클래스에 속한 멤버 함수에 대한 정의는 PosValue.cpp에 별도로 작성하는 것이다.

// PosValue.cpp 


#include "PosValue.h"

template <typename T>
void PosValue<T>::outvalue()
{
    gotoxy(x,y);
    cout << value << endl;
}

 

이렇게 되면 outvalue 함수는 PosValue.cpp 안에서만 알 수 있게 되는 것이다.

즉, 다른 모듈에서 참조할 수 없다.

일반 함수라면 원형만으로 컴파일 가능하고 링크할 때 실제 주소로 바인딩되지만

템플릿일 때는 컴파일할 때 완벽하게 구체화되어야 하므로 같은 단위파일에 있어야 했다.

(해당 파일과 include한 파일을 포함한 파일을 단위파일이라고 하자)

 

템플릿의 멤버 함수가 별도의 구현 파일에 정의되어 있다면 main을 컴파일할 때

멤버 함수가 구체화되지 않아서 문제가 된다는 말이다.

템플릿은 컴파일러에게 미리 이러저러한 타입일 것이다 얘기할 뿐

실제로 코드를 생성하는 것이 아니기 때문이다.

때문에 클래스 템플릿은 헤더에 작성하는 것이 원칙이고 설사 중복 포함하더라도 선언이라 문제되지 않는다.

 

하지만 템플릿을 헤더에 선언하면 소스를 숨길 수 없다.

기술적으로 중요한 소스는 숨기는 것이 원칙이지만 헤더 파일은 소스 형태로 배포되어 숨기기 어렵다.

이런 문제를 해결하기 위해서 C++에서는 구현 파일에 템플릿의 멤버 함수를 정의하는 export 키워드를 도입했다.

이 키워드를 써서 선언하면 멤버함수는 외부로도 알려지게 된다.

export template <typename T>
void PosValue<T>::outvalue(){...}

...

 

하지만 export를 지원하지 않는 경우도 있어서

템플릿 라이브러리는 대부분 공개되어있다.

 


 

함수의 default 인수가 있듯이 호출 시 생략된 인수에 대해서 기본적으로 적용되는 값이 있듯이

템플릿도 기본적으로 적용되는 타입을 정할 수 있다.

템플릿 선언문 자체에서 타입 인수 다음에 = 구분자로 기본 타입을 명시하면 된다.

 

template <typename T=int>
..

 

예를 들어 명시적으로 타입선언하지 않을 경우 해당 T는 int로 해석된다는 의미다.

즉, 정수형의 객체를 선언할 때는 < >를 빈 칸인채로 냅둬도 된다는 말이다.

 

하지만 클래스 템플릿과 달리 함수 템플릿 자체에서는 default 인수를 지정할 수 없다.

함수는 호출할 때 실인수의 타입을 보고 구체화할 함수를 결정하는데

실인수를 생략해 버리면 어떤 타입의 함수를 원하는지 알 턱이 없어서 그렇다.

 

때론 템플릿의 인수가 타입을 지정하는 것이 아니라 상수를 전달하기도 하는데 비타입 인수라고 한다.

 

아래의 Array 클래스는 임의 타입의 고정 크기 배열을 표현하며 값을 읽거나 변경하는 기능을 제공한다.

배열 요소의 타입을 지정하는 타입 인수와 배열의 크기를 지정하는 정수 상수를 전달받는다.

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

위에서 N이라는 상수를 불러와서 읽는다.

GetAt 함수에서는 배열의 범위를 벗어나서 Segment Fault 같은 것이 나지 않도록

0을 반환하게끔 구현했다.

 

하지만 굳이 템플릿으로 전달하지 않아도 생성자의 인수로 크기를 전달해서 동적 할당을 하는 방법도 있다.

포인터를 사용하면 필요한 만큼 할당할 수 있고 재할당도 가능하다.

하지만 동적할당을 하게 된다면 소멸자, 복사 생성자, 대입 연산자에 대해서 모두 정의해야 하고 상속관계까지 하려면.. 고려할 점이 너무 많기 때문에 부담이 커진다.

 

그래서 템플릿은 이를 정적 할당하면서도 객체마다 크기를 다르게 생성할 수 있어 간편하고 속도가 빠르고 안전하다.

Array 템플릿 자체는 비타입 인수로 크기를 지정할 수 있고 단순해진다.

그러나 크기를 다른 객체를 생성할 때마다 클래스가 구체와 되어 생겨나기 때문에 이는 단점으로 작용한다.

즉, 객체 5, 6, 7을 다 다르게 선언되기 때문에 낭비가 생긴다.

 

클래스 선언문의 비타입 인수는 상수만 사용가능하며 실행 중에 결정되는 변수는 사용할 수 없다.

즉, 템플릿은 타입 인수를 적용하여 컴파일 중에서 클래스를 만드는 것으로 모든 정보가 컴파일 중에 결정되어야 하기 때문이다.

 

 

반응형
그리드형