Advanced C++

[C++ 클래스] 템플릿 클래스와 전방 선언

로파이 2021. 3. 24. 17:51

템플릿 클래스

클래스 선언이 템플릿 인자와 함께 선언되어 인자만 달리하여 다른 클래스를 정의 및 생성 가능하다.

- 임의의 원소를 담는 리스트 클래스

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에,

  1. 해당 함수 구현 내용을 정의한다.
  2. 명시적으로 특정 템플릿 인수와 함께 클래스를 선언한다.

명시적 선언을 한 클래스는 인스턴스화한 것 처럼 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;
}

- 실행 결과