템플릿 클래스
클래스 선언이 템플릿 인자와 함께 선언되어 인자만 달리하여 다른 클래스를 정의 및 생성 가능하다.
- 임의의 원소를 담는 리스트 클래스
template<class Item>
class List
{
public:
List(long size = DEFAULT_LIST_CAPACITY);
~List();
long Count() const;
Item& Get(long index) const;
Item& First() const;
Item& Last() const;
bool Includes(const Item&) const;
void Append(const Item&);
void Prepend(const Item&);
void Remove(const Item&);
void RemoveLast();
void RemoveFirst();
void RemoveAll();
};
※ 템플릿 클래스는 반드시 헤더 파일에 모든 함수 내용을 같이 정의해야 한다.
템플릿 클래스란
템플릿의 임의의 인자(Item)에 대해 새로운 클래스를 정의하는 것과 같다. 따라서 다음 두 인스턴스화는 컴파일러에게 다음과 같이 새로운 클래스를 등록하도록 하는 것이다.
- List<int> intList; -> List<int>.h 와 List<int>.cpp (List<int>.o)
- List<string> stringList; -> List<string>.h 와 List<string>.cpp (List<string>.o)
- List<Object> objList -> List<Object>.h 와 List<Object>.cpp (List<Object>.o)
위와 같이 클래스를 등록하는 작업은 템플릿 클래스의 내용 List<T>.h을 보고 유추하여 컴파일러가 자동적으로 생성해주도록한다.
일반적인 클래스 ex) Foo class는 Foo.h 헤더 파일과 Foo.cpp 실행 파일이 컴파일 될 때 Foo.o 오브젝트 파일을 만들어내고 링커에 의해 해당 클래스를 사용하는 곳에 링크된다.
템플릿 클래스는 "Instantiation-style polymorphism"으로 헤더 파일이 컴파일 될 때가 아니라 Foo<int> inst로 인스턴스화하는 과정에서 Foo<int>.h라는 템플릿 클래스를 만드는 레시피를 참조해서 Foo<int>.o를 생성하는 것이다. 이는 모든 가능한 템플릿 인수에 대해 클래스를 등록하는 것은 불가능하고 또한 낭비이기 때문에 인스턴스화 시점에서 생성하도록 한다.
만약 인스턴스화 시점에서 Foo<int>에 대해 클래스를 등록해야하는데 참조한 템플릿 클래스 헤더 파일의 "레시피"에 함수 내용이 누락 혹은 정의되어 있지 않다면, Foo<int>.o 에는 그러한 함수가 정의되어 있지 않다.
따라서 헤더 파일에 모든 함수 내용을 같이 정의하지 않았다면, 클래스를 등록하는 과정에서 컴파일러는 해당 누락된 함수에 대해 "참조되는 확인 할 수 없는 외부 기호"의 링크 에러가 발생하게 된다.
그래도 cpp에 분리하여 정의하고 싶다면, 방법이 아에 없는 것이 아니다.
Foo.cpp에,
- 해당 함수 구현 내용을 정의한다.
- 명시적으로 특정 템플릿 인수와 함께 클래스를 선언한다.
명시적 선언을 한 클래스는 인스턴스화한 것 처럼 Foo.cpp에 해당 함수 구현을 참고하여 클래스 등록을 해준다.
Foo.h
// Foo.h
template<typename T>
class Foo
{
public:
T _val;
public:
void SetVal(T val);
};
Foo.cpp
#include "Foo.h"
template<typename T>
void Foo<T>::SetVal(T val)
{
_val = val;
}
template Foo<int>;
template Foo<float>;
관련 내용 : stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file
Why can templates only be implemented in the header file?
Quote from The C++ standard library: a tutorial and handbook: The only portable way of using templates at the moment is to implement them in header files by using inline functions. Why is this? (
stackoverflow.com
클래스 전방 선언 Forward Declarartion - 헤더 파일 순환 참조 문제
두 헤더 파일이 동시에 서로 include를 하는 것으로 전처리기가 선언되어 있다면, 컴파일러가 include를 위해 헤더 파일을 확인하면서 무한 루프에 빠지게 된다.
보통 두 개이상의 클래스가 서로 포함 관계가 있고 이를 위해 사용자가 헤더 파일에 관련된 클래스를 include 하면서 발생한다.
// Parent.h
#include "Child.h"
class Parent
{
private:
list<Child*> childList;
public:
Parent();
~Parent();
void AddChild(Child*);
};
// Child.h
#include "Parent.h"
class Child
{
private:
Parent* _parent;
public:
Child(Parent*);
~Child();
};
위 문제의 해결법은 잘 알려져 있다 싶이 하나의 헤더 파일 혹은 두 헤더 파일 모두에 전방 선언 (forward declaration)을 통해 해결 가능하다.
- Child 헤더파일에 Parent 클래스를 전방 선언
// Child.h
#include "Parent.h"
class Parent;
class Child
{
private:
Parent* _parent;
public:
Child(Parent*);
~Child();
};
템플릿 클래스의 전방 선언
템플릿 클래스도 전방 선언이 가능하며 다음과 같이 사용한다.
template<typename E>
class Foo;
template<typename T>
class Boo
{
private:
Foo<T>* p;
public:
Boo(Foo<T>*);
};
이러한 복잡한 include 관계의 예로, List<Item> 객체와 이에 대한 반복자 객체 ListIterator<Item>를 정의할 때 두 클래스 모두 템플릿 클래스이고 ListIterator<Item> 클래스 내에 List<Item>* 포인터가 관리될 수 있고 List도 ListIterator 인스턴스를 만들어 자신에 대한 반복자를 반환해야할 수도 있다. 그렇다면 서로 헤더 파일을 include 할 수 있기 때문에 위와 같이 템플릿 클래스 전방 선언이 필요하다.
- 관련 내용 : 2021.03.24 - [GoF Design Pattern] - [디자인 패턴] (8) 반복자 Iterator
관련 공부용 예시 코드
#pragma once
#include <iostream>
typedef char* Time;
void Porting(Time dst, const Time src)
{
int i = 0;
while (src[i] != '\0')
{
dst[i] = src[i];
++i;
}
}
class TimeContext
{
protected:
Time _absolute_time_space;
public:
TimeContext(const Time &time_space)
{
_absolute_time_space = new char[128];
memset(_absolute_time_space, 0, 128);
Porting(_absolute_time_space, time_space);
}
~TimeContext()
{
delete _absolute_time_space;
}
virtual void From() const { std::cout << "You are from " << _absolute_time_space << std::endl; }
};
Present 클래스는 Past* 인스턴스를 매개 변수로 생성할 수 있다. 따라서 Past 클래스 함수 정의가 필요하다.
#pragma once
#include "TimeContext.h"
template<typename E>
class Past;
template<typename T>
class Present : public TimeContext
{
private:
Past<T>* _wormhole = nullptr;
public:
~Present();
Present(T context);
Present(Past<T>* from_past);
void From() const
{
if (_wormhole)
{
_wormhole->From();
return;
}
return TimeContext::From();
}
};
template<typename T>
Present<T>::Present(Past<T>* from_past)
:
TimeContext((Time)"Unknown")
{
_wormhole = from_past;
}
template<typename T>
Present<T>::Present(T context)
:
TimeContext(context)
{
}
template<typename T>
Present<T>::~Present()
{
if (_wormhole)
{
delete _wormhole;
_wormhole = nullptr;
}
}
Past 클래스는 Present* 인스턴스를 직접 생성한다. 따라서 Present 클래스 정의가 필요하다.
#pragma once
#include "TimeContext.h"
template<typename E>
class Present;
template<typename T>
class Past : public TimeContext
{
public:
Past(T context)
:
TimeContext(context)
{
}
Present<T>* BackToTheFuture()
{
return new Present<T>(this);
}
};
두 헤더 파일에서 순환 참조 없이 사용자 코드에서 include하여 사용가능 하다.
#include <iostream>
#include "Present.h"
#include "Past.h"
int main()
{
Time yesterday = (Time)"2020/03/23";
Time today = (Time)"2020/03/24";
Past<Time>* past = new Past<Time>(yesterday);
Present<Time>* normal = new Present<Time>(today);
Present<Time>* time_traveler = past->BackToTheFuture();
normal->From();
time_traveler->From();
delete past, time_traveler;
return 0;
}
- 실행 결과

'Advanced C++' 카테고리의 다른 글
| [Modern C++] (3-2) 현대적 C++에 적응하기 (0) | 2021.03.26 |
|---|---|
| [Modern C++] (3-1) 현대적 C++에 적응하기 (0) | 2021.03.25 |
| [Modern C++] (2) auto 키워드 (0) | 2021.03.22 |
| [Modern C++] (1) 형식 연역 (0) | 2021.03.19 |
| [C++] 스마트 포인터 shared_ptr/unique_ptr/weak_ptr (0) | 2021.03.06 |