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를 통해 인자에 따라 다른 처리를 해줄 수 있다.