C++/[ecourse] C++ Template

10. 템플릿 디자인

헛둘이 2022. 10. 9. 13:24
Thin Template
  • 템플릿의 단점은 특정 타입에 대한 코드를 만들어냄으로써 발생하는 코드메모리의 증가임
  • 이를 방지하기 위해 고안한 기술
  • T를 사용하지 않는 다른 인자들은 기반 클래스로 올리고 파생 클래스에서 T를 사용하는 함수들만 사용
// 기존 Vector
template<typename T> 
class Vector
{
	T* buff;
	int sz;

public:
	int size() const {}
	bool empty() const {}
	void push_front(const T& a) {}
	T& front() {}
};
  • 이런 기존 모양의 Vector를 아래와 같이 변경
// 변경 후 Vector
class VectorBase
{
protected:
	int sz;
public:
	int size() const {}
	bool empty() const {}
};

template<typename T> 
class Vector : public VectorBase
{
	T* buff;

public:
	void push_front(const T& a) {}
	T& front() {}
};
  • 이렇게 쓰면 Vector<int>, Vector<double>이 만들어져도 T가 들어간 함수들만 생성됨
  • 여기서 더 나아가서 기존 T를 사용하던 함수들을 void*로 바꾸고 전부 VectorBase로 올림
  • 캐스팅은 파생 클래스에서 해결
class VectorBase
{
protected:
	void* buff;
	int sz;
public:
	int size() const {}
	bool empty() const {}
	void push_front(const void* a) {}
	void* front() {}
};

template<typename T> 
class Vector : public VectorBase
{
	
public:
	inline void push_front(const T& a) {
		VectorBase::push_front(static_cast<void*>(a));
	}
	inline T& front() {
		return static_cast<T&>(VectorBase::front());
	}
};
  • 파생 클래스는 하는 일이 캐스팅밖에 없으므로 함수를 inline으로 박아주면 됨
  • 코드메모리에 대한 오버헤드가 거의 없음

 

 


CRTP (Curiously Recurring Template Pattern)
  • 부모 클래스를 만드는 시점에서 자식 클래스를 T로 받아서 사용하도록 설계하는 기법
  • 부모 클래스의 this를 자식 클래스 T로 형변환함으로써 가상함수처럼 동작하도록 할 수 있다.
#include <iostream>

template<typename T>
class Window
{
public:
	void msgLoop()
	{
		static_cast<T*>(this)->OnClick();
	}

	void OnClick() { std::cout << "Window OnClick!" << std::endl; }
};

class FrameWindow : public Window<FrameWindow>
{
public:
	void OnClick() { std::cout << "FrameWindow OnClick!" << std::endl; }
};

int main()
{
	FrameWindow w;
	w.msgLoop();
}
  • 부모 클래스의 msgLoop 함수는 내부적으로 OnClick을 호출하는데,
  • OnClick을 호출하는 주체는 Window이기 때문에 Window->OnClick()이 불리는게 일반적이지만
  • 자식 클래스로부터 넘겨 받은 T로 this를 캐스팅함으로써 자식 클래스의 OnClick을 호출하게 된다.  
  • 아래는 CRTP를 활용한 싱글톤 기법

 

#include <iostream>

template<typename T>
class Singleton
{
protected:
	Singleton() {}

private:
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

public:
	static T& getInstance()
	{
		static T instance;
		return instance;
	}
};

class Mouse : public Singleton <Mouse>
{};

int main()
{
	Mouse& mouse = Mouse::getInstance();
	std::cout << typeid(mouse).name() << std::endl;
}
  • 이렇게 사용하면 싱글톤을 만드는 코드를 확연히 줄일 수 있다.
  • 안드로이드 프레임워크가 이렇게 되어있음

 

 

 

 


SFINAE (Substitution Failure Is Not An Error)
  • 치환 실패는 에러가 아니다
  • 특정 타입을 제외한 타입이 템플릿 인자로 넘어올 때 편리하게 예외 처리할 수 있는 기법
#include <iostream>

template<typename T>
void foo(T a) { std::cout << "T" << std::endl; }

//void foo(int a) { std::cout << "int" << std::endl; }

void foo(...) { std::cout << " ... " << std::endl; }

int main()
{
	foo(3);
}
  • 정확한 타입이 있으면 그 타입과 매칭되는걸 exactly matching이라고 한다.
  • 그리고 그게 없다면 템플릿과 가변인자 함수 중 템플릿을 먼저 호출한다.
  • 템플릿이 int형을 만들어낼 수 있기 때문이다.
  • 치환이 실패하면 컴파일 에러가 아니라 함수 후보군에서 제외하는 특성을 이용함
#include <iostream>

template<typename T>
typename T::type foo(T a) { std::cout << "T" << std::endl; }

void foo(...) { std::cout << " ... " << std::endl; }

int main()
{
	foo(3);
}
  • int가 넘어갈 경우 template 버전 foo는 int::type이 존재하지 않으므로 이 함수는 후보군에서 제외됨
  • 따라서 foo (...)가 호출됨
  • 이걸 이용할 수 있게 해주는 게 C++ 표준의 enable_if
#include <iostream>
#include <type_traits>

template<bool B, typename T = void>
struct enable_if
{
};

template<typename T>
struct enable_if<true, T>
{
	typedef T type;
};

template<typename T>
typename enable_if<std::is_integral<T>::value>::type 
foo(T a) { std::cout << "T" << std::endl; }

void foo(...) { std::cout << " ... " << std::endl; }

int main()
{
	foo(3.4);
}
  • enable_if를 통해 인자에 따라 다른 처리를 해줄 수 있다.