[C++] 14. 템플릿 #2

kkado·2023년 10월 20일
0

열혈 C++

목록 보기
14/16
post-thumbnail

💬 윤성우 님의 <열혈 C++ 프로그래밍> 책을 혼자 공부하며 배운 내용을 정리합니다. 글의 모든 내용은 책에서 발췌하였습니다.


클래스 템플릿과 배열 클래스 템플릿

전 글에서 클래스 템플릿과 그 응용 사례들을 알아보았다.

Point<int> arr(50);

이렇게 선언하면 int형 데이터를 저장할 수 있다.
그럼 Point<int> 템플릿 클래스와 이전에 사용했던 BoundCheckArray 클래스 템플릿을 이용해 배열을 만드려면 어떻게 해야 할까.

간단하다. 자료형이 Point<int>일 뿐 일반 클래스를 이용하는 것과 똑같다.

BoundCheckArray<Point<int>> oarr(50);

만약 저장 대상 자료형이 Point<int>가 아니라 포인터형이라면 아래와 같이 생성할 수 있다.

BoundCheckArray<Point<int*>> oparr(50);

약간의 가독성 향상을 위해 typedef를 사용하면 다음과 같다.

typedef Point<int>* POINT_PTR;
BoundCheckArray<POINT_PTR> oparr(50);

템플릿 클래스 객체를 인자로 받는 일반함수의 정의와 friend 선언

템플릿 클래스 자료형을 대상으로도 일반 함수의 정의가 가능하고, 클래스 템플릿 내에서 이러한 함수를 대상으로 friend 선언도 가능하다.

#include <iostream>
using namespace std;

template <typename T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x=0, T y=0): xpos(x), ypos(y)
    {}
    void showPosition()
    {
        cout << "[" << xpos << ", " << ypos << "]\n";
    }

    friend Point<int> operator+(const Point<int>&, const Point<int>&);
    friend ostream& operator<<(ostream&, const Point<int>&);
};

Point<int> operator+(const Point<int>& pos1, const Point<int>& pos2)
{
    return Point<int>(pos1.xpos + pos2.xpos, pos1.ypos + pos2.ypos);
}

ostream& operator<< (ostream& os, const Point<int>& pos)
{
    os << "[" << pos.xpos << ", " << pos.ypos << "]\n";
    return os;
}

int main()
{
    Point<int> pos1(2, 4);
    Point<int> pos2(4, 8);
    Point<int> pos3 = pos1 + pos2;

    cout << pos1 << pos2 << pos3;
}
[2, 4]
[4, 8]
[6, 12]

Point<int> operator+ 함수를 언뜻 보면 함수 템플릿인 것처럼 보이지만 + 연산자를 오버로딩하는 일반 함수이다. 반환형이 Point<int> 일 뿐이다.


클래스 템플릿의 특수화

함수 템플릿의 특수화에 대한 기억을 되살려보자.
특수화의 이유는 함수 템플릿을 이용해 함수를 일반화하는 와중에 특정 자료형에 예외상황을 추가하여 다르게 동작하게끔 하기 위함이었다.

이와 마찬가지로 클래스 템플릿을 특수화하는 이유 역시 특정 자료형을 기반으로 생성된 클래스는 특수하게 동작하도록 하기 위함이다.

아래와 같이 정의된 클래스 템플릿이 존재할 때,

template <typename T>
class Simple
{
public:
    Simple(T num) 
    {}
};

int 자료형에 대해 특수화를 진행한 템플릿 클래스는 다음과 같이 정의한다.

template <>
class Simple
{
public:
	Simple (int num)
    {}
};

이제 Simple<int> obj 처럼 객체를 생성하면 특수화된 클래스 템플릿을 이용해 객체가 생성된다.

여담) 일부 컴파일러에서는 아직 클래스 템플릿을 지원하지 않는 경우도 있다고 하니 컴파일 오류가 나더라도 당황하지 않아도 된다.


클래스 템플릿의 부분 특수화

함수 템플릿을 정의할 때 여러 가지 타입을 사용할 수 있다는 것은 이해가 쉬웠을 것이다.

template <class T1, class T2>
void showData(double num)
{
    cout << (T1)num << ", " << (T2)num << "\n";
}

가령 이런식으로 작성된 것이다. 딱히 특별한 것은 없고, 클래스에서도 똑같이 적용 가능하다. 여러 가지 타입을 사용해서 클래스 템플릿을 특수화하면 다음과 같다.

template <>
class Simple<char ,int> {};

그리고 이렇게 여러 타입을 사용할 때 부분적으로도 가능하다.

template <T1>
class Simple<T1, int> {};

아래는 이것을 사용한 간단한 예제이다.

template <typename T1, typename T2>
class Simple
{
public:
    void whoAreYou()
    {
        cout << "size of T1 : " << sizeof(T1) << "\n";
        cout << "size of T1 : " << sizeof(T2) << "\n";
        cout << "<typename T1, typename T2>\n"; 
    }
};

template <>
class Simple<int, double>
{
public:
    void whoAreYou()
    {
        cout << "size of int : " << sizeof(int) << "\n";
        cout << "size of double : " << sizeof(double) << "\n";
        cout << "<int, double>\n";
    }
};

template <typename T1>
class Simple<T1, double>
{
public:
    void whoAreYou()
    {
        cout << "size of T1 : " << sizeof(T1) << "\n";
        cout << "size of double : " << sizeof(double) << "\n";
        cout << "<T1, double>\n";
    }
};

int main()
{
    Simple<char, double> obj1;
    obj1.whoAreYou();
    Simple<int, long> obj2;
    obj2.whoAreYou();
    Simple<int, double> obj3;
    obj3.whoAreYou();
}
size of T1 : 1
size of double : 8
<T1, double>
size of T1 : 4
size of T1 : 8
<typename T1, typename T2>
size of int : 4
size of double : 8
<int, double>

obj1은 부분 특수화된 템플릿 클래스, obj2는 일반 템플릿 클래스, obj3은 특수화된 템플릿 클래스이다.

여기서 확인해 볼 수 있는 것은 obj3이다. <int, double> 형으로 만든 클래스는 <T1, T2>는 물론 <T1, double> 도 만족하고 <int, double> 도 만족한다. 그런데 결과를 보면 <int, double> 형으로 생성된 것을 볼 수 있다.

이처럼 여러 클래스에 대응될 수 있는 경우 전체 특수화가 우선 순위가 가장 높고, 그 다음으로 부분 특수화, 그리고 일반 클래스 템플릿이 적용된다.

가장 대응되기 어렵고 세부적으로 명시된 것이 우선시되는 것이 당연하다고 볼 수도 있다.


템플릿 인자

템플릿의 매개변수에는 변수의 선언이 올 수 있다.

템플릿 클래스를 정의할 때 사용하는 <typename T> 에서 T를 가리켜 '템플릿 매개변수' 라고 한다. 그리고 이러한 매개변수에 전달되는 자료형 정보를 '템플릿 인자' 라고 한다. 그리고 템플릿 매개변수의 선언에는 변수도 올 수 있다.

template <typename T, int len>
class SimpleArray
{
private:
	T arr[len];
public:
	T& operator[] (int idx)
    {
    	return arr[idx];
    }
};

그리고 이를 기반으로 다음의 형태로 객체 생성도 가능하다.

SimpleArray<int, 5> arr1;
SimpleArray<int, 7> arr2;

눈여겨 볼 점은 위의 두 객체는 '서로 다른 클래스 자료형' 이라는 점이다.
이들은 각각 SimpleArray<int, 5> 형, SimpleArray<int, 7> 형이다.

그런데 이렇게 배열의 길이를 정하기 위해서 템플릿 매개변수에 정수를 전달하는 건 낯설고 번거로운데 굳이 이렇게 해야 할까?

만약 길이가 다른 두 배열 객체끼리는 덧셈을 못 하게 해야 할 경우에는 이 방법이 유용할 수 있다. 길이가 다름은 곧 자료형이 다름을 의미하기 때문에, 대입이나 복사가 불가능하기 때문이다. 즉 길이의 같은 배열에 대해서만 대입을 허용하기 위한 추가적인 코드가 필요없게 된다.


또한 함수의 매개변수에 디폴트 값을 지정할 수 있듯이 템플릿 매개변수에도 디폴트 값 지정이 가능하다.

template <typename T=int, int len=5>
class SimpleArray
{
	...
};

그리고 이렇게 선언된 템플릿 클래스를 사용할 때에 기본값을 사용한다 할지라도 템플릿 클래스의 객체 생성을 의미하는 괄호 <>는 추가해 주어야 한다. 비록 그 안을 비워둘지라도 말이다.

int main()
{
	SimpleArray<> arr;
    arr[0] = 1;
    cout << arr[0] << "\n";
}

템플릿과 static

in 함수 템플릿

static 키워드의 특징을 딱 한 줄로 요약하자면 '딱 한 번 초기화되고 그 값을 계속 유지한다' 라고 할 수 있다.

함수 템플릿에도 이 static 키워드를 추가할 수 있다. 다음 예제를 봅시다

template <typename T>
void showStaticValue()
{
    static T num = 0;
    num += 1;
    cout << num << " ";
}

int main()
{
    showStaticValue<int>();
    showStaticValue<int>();
    showStaticValue<int>();
    cout << "\n";


    showStaticValue<double>();
    showStaticValue<double>();
    showStaticValue<double>();
    cout << "\n";

    showStaticValue<long>();
    showStaticValue<long>();
    showStaticValue<long>();
    cout << "\n";
}
1 2 3 
1 2 3 
1 2 3 

간단한 함수 템플릿을 만들었고 이 안에 static 변수를 선언했다.
그리고 메인 함수에서 3가지 자료형으로 함수를 호출하고 있다.

실행 결과를 보면 알 수 있듯이, 컴파일러에 의해서 만들어진 템플릿 함수별로 static 지역 변수가 각각 따로 유지되고 있음을 볼 수 있다.

함수 템플릿 뿐만 아니라 클래스 템플릿 역시 static 멤버 변수를 이용해서 같은 클래스 간 다른 객체들 간에 공유가 가능하다.

template <typename T>
class SimpleStatic
{
private:
    static T num;
public:
    void addNum(T n)
    {
        num += n;
    }
    void showNum()
    {
        cout << num << "\n";
    }
};

template <typename T>
T SimpleStatic<T>::num = 0;

int main()
{
    SimpleStatic<int> obj1;
    SimpleStatic<int> obj2;
    
    obj1.addNum(5);
    obj2.addNum(10);
    obj1.showNum();
    obj2.showNum();

    SimpleStatic<double> obj3;
    SimpleStatic<double> obj4;
    
    obj3.addNum(3);
    obj4.addNum(5);
    obj3.showNum();
    obj4.showNum();
}
15
15
8
8

T SimpleStatic<T>::num = 0; 을 통해 초기화를 해 줘야 한다는 것을 명심하자 !


'template <typename T>''template <>'

언제 'template <typename T>' 를 사용하고, 언제 'template <>' 을 사용할까?

앞서 템플릿을 처음 사용할 때 template <typename T>를 다음과 같이 설명했다. 내가 작성했던 것을 토씨 하나 안 바꾸고 가져와 보면,

이 T라는 것이 '자료형을 결정하지 않았다' 라는 뜻을 나타내기 위해, 아래와 같이 template <typename T>을 붙여 완성한다.

라고 하였다. 자료형을 아직 결정하지 않았다는 것은 바꿔 말하면, 사용할 때 결정해야 한다는 뜻이다. 즉 'template <typename T>' 이라는 문법을 위에서 조금 더 확장하자면 'T의 타입은 아직 정해지지 않았고 결정해 주어야 한다' 이다.

그런데 만약에 어떤 클래스를 특수화해서 더 이상 어떤 타입을 정해줄 필요가 없어졌다고 해보자. 그러면 이제 '어떤 타입 미정의 자료형이 없으므로' 괄호 <> 안에 들어갈 것이 없게 된다. 그래도 괄호 <> 는 '이것이 템플릿 관련 정의이다' 라는 것을 명시하기 위해 필요하기 때문에 남겨 두고 안을 비운다.

template <>
class Simple<int>
{
public:
	int Simple(int n) {}
};

템플릿 static 멤버 변수 초기화의 특수화

위에서 static 을 다룰 때 아래와 같이 static 변수의 초기화를 진행했다.

template <typename T>
T SimpleStatic<T>::num = 0;

만약 int형을 사용할 때는 0으로, double 형을 사용할 때는 5로 초기화하고 싶으면, 이 문장 역시 특수화가 가능하다.

template <typename T>
T SimpleStatic<T>::num = 0;

template <>
double SimpleStatic<double>:: num = 5;

profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글