
Template class
이제까지 function template으로 여러 다른 데이터 타입과 함께 동작하는 함수를 만들어 사용했다
class template도 마찬가지로 여러 타입에 대응할 수 있는 클래스를 만들어준다
예를 들어 array를 만들어주는 class를 한번 설계해보자
class IntArray
{
public:
IntArray(int inLength)
{
data = new int[inLength]{};
length = inLength;
}
~IntArray()
{
delete[] data;
}
int getArrayLength() { return length; }
int& operator[](int index)
{
//범위 체크
return data[index];
}
private:
int length{};
int* data{};
};
이 IntArray클래스를 가지고 우리는 배열처럼 사용이 가능해진다, 하지만 double형 배열을 만들어주는 클래스가 필요하다면? 내부적으로 로직은 전부 동일한데 타입만 다른 class DoubleArray를 선언해야 하는가?
이럴때 클래스 템플릿을 유용하게 사용할 수 있다 (여러 다른 데이터 타입과 함께 동작하는 클래스)
IntArray를 만들고 DoubleArray를 만들고 floatArray를 만들고 다 만들면 유지보수가 굉장히 어렵고 비효율적이다
템플릿 클래스는 함수 템플릿과 비슷하게 만든다
template <typename T>
class Array
{
public:
Array(int inLength)
{
data = new T[inLength]{}; //int형 배열 대신 T형 배열 동적할당
length = inLength;
}
~Array()
{
delete[] data;
}
int getArrayLength() { return length; }
T& operator[](int index) //operator[]의 반환형을 T&로 변경
{
//범위 체크
return data[index];
}
private:
int length{};
T* data{}; //element타입을 T*로 변경
};
int main()
{
Array<int> myArray{ 10 };
Array<double> myDoubleArray{ 10 };
}
이때 템플릿 함수는 클래스 외부에서 정의될 수 있다
template <typename T>
class Array
{
public:
Array(int inLength)
{
data = new T[inLength]{};
length = inLength;
}
~Array()
{
delete[] data;
}
int getArrayLength() { return length; }
T& operator[](int index);
private:
int length{};
T* data{};
};
//외부에서 정의
template <typename T>
T& Array<T>::operator[](int index) //Array<T>::로 함수를 정의해야 한다
{
return data[index];
}
클래스 외부에서는 반드시 클래스명< T >::로 접근해야 하고 클래스 내부에서는 그냥 < T >를 붙히지 않아도 된다 (컴파일러가 알아서 처리함)
//클래스 내부의 생성자
Array(const Array& InArray) = delete; //ok, const Array&로 그냥 사용해도 됨
//클래스 외부의 함수
template <typename T>
T& Array<T>::operator[](int index){ }; //ok, 클래스 외부이니 Array<T>::로 사용해야 함
이러함 클래스 템플릿은 함수 템플릿과 동일한 방식으로 인스턴스화 된다
컴파일러는 클래스 템플릿과 함수 템플릿이 사용되면 실질적으로 필요로 하는 데이터 타입으로 교체된 복사본을 만들어내고 해당 복사본을 컴파일한다
Array<int> intArr{10};
을 만나게 되면 Array클래스 템플릿에 int가 들어간 버전의 복사본이 만들어지고 해당 복사본을 컴파일 한다는 의미이다
따라서 템플릿 클래스를 전혀 사용하지 않는다면 해당 코드를 컴파일조차 하지 않는다
이러한 템플릿 클래스는 컨테이너 클래스를 구현할 때 굉장히 유리하다 (컨테이너는 다양한 타입을 담아야 하기 때문)
템플릿 사용 시 주의할 점
우선 template은 함수나 클래스가 아니고 그저 설계도일 뿐이다, 따라서 함수나 클래스 사용법과 분리되어야 한다
일반적으로 template이 아닌 클래스를 구현할 때 클래스의 정의를 .h에 작성하고 멤버 함수의 정의를 .cpp에 정의하게 된다, 이렇게 되면 클래스의 정의와 멤버함수의 정의는 별개의 파일로 컴파일된다
하지만 template에서는 해당 방식이 동작하지 않는다
//Array.h
template <typename T>
class Array
{
public:
Array(int inLength)
{
data = new T[inLength]{};
length = inLength;
}
~Array()
{
delete[] data;
}
int getArrayLength() { return length; }
T& operator[](int index);
private:
int length{};
T* data{};
};
//Array.cpp
template <typename T>
T& Array<T>::operator[](int index)
{
//범위 체크
return data[index];
}
//main.cpp
#include "Array.h"
int main()
{
Array<int> intArr{10};
intArr[0] = 20;
}
이는 컴파일은 되지만 LNK에러를 발생시킨다
함수 템플릿과 마찬가지로 해당 클래스 템플릿이 인스턴스화 되기 위해서는 컴파일러는 전체 클래스 템플릿의 정의와 선언을 모두 볼 수 있어야 한다 (Array.h만 include했기 때문에 실제 Array 템플릿 클래스의 멤버 함수 템플릿 정의를 볼 수 없음 -> 함수 템플릿 인스턴스화 불가)
이러한 문제를 해결하기 위해 모든 템플릿의 선언과 정의를 헤더파일에 넣어서 사용하는 방법이 있다
(위의 예시에서는 T& Array< T >::operator[](int index)의 정의를 Array.h에 넣는것)
하지만 헤더파일에 전부 넣게 되면 헤더가 너무 길고 복잡해질 수 있으며 해당 파일이 많이 include된다면 컴파일 타임이 늘어날 수 있다, 그렇다면 .inl(inline파일)에 넣고 해당 .inl파일을 #include하여 사용하는것도 방법 중 하나이다
//Array.h
#ifndef ARRAY_H
#define ARRAY_H
template <typename T>
class Array
{
public:
Array(int inLength)
{
data = new T[inLength]{};
length = inLength;
}
~Array()
{
delete[] data;
}
int getArrayLength() { return length; }
T& operator[](int index);
private:
int length{};
T* data{};
};
#include "Array.inl"
#endif
//Array.cpp
//아무런 정의 없음
//Array.inl
#include "Array.h"
template <typename T>
T& Array<T>::operator[](int index)
{
//범위 체크
return data[index];
}
//main.cpp
int main()
{
Array<int> intArray{ 5 };
intArray[0] = 100;
return 0;
}
template non-type param
template에는 Type칸과 Value칸이 들어갈 수 있다, 이때 여기서 Value칸을 template non-type param이라고 한다
Type칸은 다음과 같다
template <typename T>에서 T
Value칸은 다음과 같다
template <int N>에서 N
여기서 N은 자료형이 아닌 정해진 값이 들어간다, 이 값은 컴파일 할 때 이미 결정되어있어야 한다 (constexpr)
템플릿 코드를 실제 사용하는 타입들을 사용하는 복사본을 만들 때 해당 template 값들이 전부 있어야 하기 때문에 constexpr만 허용된다 (constexpr이 아니면 컴파일러가 값을 모를 수 있다)
int a{ 10 };
Array<int, a> intArray{}; //constexpr이 아니기때문에 error
template non-type param의 타입으로 들어갈 수 있는건 다음과 같다
물론 template type param과 template non-type param은 같이 사용될 수 있다 (단일로도 사용 가능)
template <typename T, int size>
class Array
{
public:
Array()
{
data = new T[size]{};
length = size;
}
~Array()
{
delete[] data;
}
int getArrayLength() { return length; }
T& operator[](int index)
{
return data[index];
}
private:
int length{};
T* data{};
};
int main()
{
Array<int, 10> intArray{};
intArray[0] = 100;
return 0;
}
위에서는 동적할당을 사용했지만 사실 template non-type param을 사용한다면 굳이 동적할당할 필요가 전혀 없다
어차피 들어오는 값이 컴파일타임에 이미 결정되어 있는 constexpr값이기 때문에 해당 클래스 내부 멤버 변수로 배열을 생성하고 클래스 객체를 { }안(ex) int main(){ })에 선언하여 stack영역에 올리고 { }영역을 벗어날 때 알아서 클래스 객체가 소멸되고 배열도 같이 자동 소멸시키는게 더 좋은 방식일 수 있다
template <typename T, int size>
class Array
{
public:
T myArr[size]{};
T& operator[](int index)
{
return myArr[index];
}
};
int main()
{
Array<int, 10> intArray{}; //Array 템플릿 클래스 타입 객체 생성과 동시에 배열 생성
intArray[0] = 100;
return 0;
} //intArray는 stack에 올라간 객체이기 때문에 main()종료 후 멤버변수인 myArr배열도 자동으로 소멸됨
함수 템플릿 특수화
특정 타입에 대해 함수 템플릿을 인스턴스화 할 때 컴파일러는 템플릿 함수의 복사본을 만들고 들어간 특정 타입으로 교체한다
이렇게 나온 함수들은 각 타입에 대해 동일한 구현을 갖게 된다, 이때 특정 데이터 타입에 대해 다른 구현을 처리하고 싶을때 함수 템플릿 특수화 처리를 해야 한다
다음은 간단한 기본적인 함수 템플릿 구현이다
template <typename T>
void Foo(const T& t)
{
std::cout << t << '\n';
}
int main()
{
Foo<int>(10);
Foo(10.5f);
return 0;
}
만약 여기서 Foo(const T&) 함수에 대해서 타입이 double일때만 구현을 다르게 처리하려면 어떻게 해야할까? 일반적인 방법으로는 함수 오버로딩을 사용하는 방법이 있다
template <typename T>
void Foo(const T& t)
{
std::cout << t << '\n';
}
void Foo(double d)
{
std::cout << "double: " << d << '\n';
}
int main()
{
Foo<int>(10);
Foo(10.5);
return 0;
}
Foo(10.5)를 호출할 때 Foo(double)이 정의된 것을 보고 Foo(const T&)를 인스턴스화 하지 않고 Foo(double)을 사용한다, 따라서 dobule: 10.5가 나오게 된다
C++의 함수 오버로딩 규칙에 따르면 템플릿 함수보다 정의된 함수의 우선순위가 더 높은걸 알 수 있다
또 다른 방법은 메인 주제인 함수 템플릿 특수화이다
모든 템플릿 매개변수가 특수화되면 full specialization이고 템플릿 매개변수중 일부만 특수화되면 partial specialization이라고 한다
템플릿 특수화를 위해서는 컴파일러는 우선 기본 템플릿 선언을 봐야 한다
//기본 템플릿 선언
template <typename T>
void Foo(const T& t)
{
std::cout << t << '\n';
}
//full 템플릿 특수화 (template<>에 매개변수가 없음)
template<>
void Foo<double>(const double& d)
{
std::cout << "double: " << d << '\n';
}
full 템플릿 특수화는 모든 매개변수를 특수화 하기 때문에 template<>에 매개변수가 없고 기본 템플릿과 완전 동일한 시그니처를 사용해야 한다 (따라서 위에서 double은 const&로 사용할 필요가 없지만 맞춰준것 -> 유연성이 떨어짐)
만약 Foo()를 호출했을 때 비 템플릿 함수와 동일한 템플릿 함수 특수화가 존재한다면 비 템플릿 함수가 우선시 된다
또한 full 템플릿 특수화는 암시적으로 inline이 아니기 때문에 ODR위배를 방지하기 위해 inline으로 선언해야 한다 (부분 특수화는 암시적으로 inline)
일반적으로 비 템플릿 함수로 오버로딩해서 같은 동작을 만들 수 있거나 혹은 템플릿의 변형임을 명시하고 싶지 않다면 굳이 템플릿 특수화는 권장하지 않는다
클래스 템플릿 특수화
클래스 템플릿도 함수 템플릿 특수화와 마찬가지로 특정 타입에 대해 특수화가 가능하다
다음과 같은 배열 클래스 템플릿이 있다고 가정해보자
template<typename T>
class Foo
{
public:
void SetArr(int index, T value)
{
arr[index] = value;
}
T GetArrElement(int index) const
{
return arr[index];
}
private:
T arr[10];
};
int main()
{
Foo<int> intFoo;
for (int i = 0; i < 10; ++i)
{
intFoo.SetArr(i, i);
}
Foo<bool> boolFoo;
for (int i = 0; i < 10; ++i)
{
boolFoo.SetArr(i, i & 3);
}
return 0;
}
Foo클래스는 클래스 템플릿으로 여러 타입에 대한 element를 멤버 변수 배열에 넣는 기능을 가지고 있다
따라서 Foo< int >, Foo< bool >등 다양한 타입의 element로 채울 수 있다 (int와 bool로 템플릿 인스턴스화가 된 것)
하지만 위 코드에서 Foo< bool >의 사용은 굉장히 비효율적이라고 볼 수 있다
bool은 결국 T,F 두개의 정보만을 나타내기 때문에 1bit만으로 표현이 가능한데, CPU는 1byte보다 작은 단위의 메모리 주소를 지정할 수 없다
따라서 1bit만 필요한데 8bit를 사용하는 비효율성이 나타나게 된다 (7bit는 낭비인셈)
그렇다면 bool일 경우에는 다른 방식으로 구현하는게 더 좋을것이고 이럴때 클래스 템플릿 특수화를 사용하면 된다
클래스 템플릿 특수화를 하게 되면 기존의 템플릿 클래스와 완전히 독립적인 클래스로 취급된다, 따라서 해당 특수화 된 클래스의 모든것을 변경할 수 있다
템플릿 특수화도 일반 템플릿과 마찬가지로 컴파일러가 사용하기 위해서는 전체 정의가 있어야 하고 특수화 되지않은 클래스 템플릿이 정의되어 있어야 한다
template<typename T>
class Foo
{
public:
void SetArr(int index, T value)
{
arr[index] = value;
}
T GetArrElement(int index) const
{
return arr[index];
}
private:
T arr[10];
};
template<>
class Foo<bool>
{
public:
void SetArr(int index, bool value)
{
//index 2 -> 00000100
auto mask{ 1 << index };
if (value) // 비트를 켜려는 경우 (true)
arr |= mask; // 비트 OR 연산을 사용해 해당 비트 켜기
else // 비트를 끄려는 경우 (false)
arr &= ~mask; // 마스크를 반전시킨 후 비트 AND 연산을 사용해 해당 비트 끄기
}
bool GetArrElement(int index)
{
//가져오려는 비트가 어디인지 알아내기
auto mask{ 1 << index };
// 비트 AND 연산을 통해 관심 있는 비트의 값을 가져온다
// 그 후 bool 타입으로 암시적 형변환
return (arr & mask);
}
private:
std::uint8_t arr{};
};
Foo<bool> boolFoo;
for (int i = 0; i < 10; ++i)
{
boolFoo.SetArr(i, i & 3);
}
이렇게 사용하게 되면 Foo를 bool타입으로 특수화한 템플릿을 사용하게 된다
멤버 함수 특수화
다음과 같은 코드가 있다고 가정해보자
template <typename T>
class Storage
{
private:
T m_value{};
public:
Storage(T value) : m_value{ value } {}
void print()
{
std::cout << m_value << '\n';
}
};
int main()
{
Storage i{ 5 };
Storage d{ 6.7 };
i.print();
d.print();
}
위에서는 비멤버 함수 템플릿 특수화를 구현했는데 다음과 같이 멤버 함수 템플릿 특수화를 구현하여 double 타입을 출력할 때 특수화를 시키고 싶으면 어떻게 할까?
template <typename T>
class Storage
{
private:
T m_value{};
public:
Storage(T value) : m_value{ value } {}
void print()
{
std::cout << m_value << '\n';
}
};
template<>
class Storage<double>
{
private:
double m_value{};
public:
Storage(double value) : m_value{ value } {}
void print();
};
void Storage<double>::print()
{
std::cout << "Double value: " << m_value << '\n';
}
템플릿 클래스를 특수화 하고 해당 멤버함수를 특수화 된 템플릿::함수로 정의해서 사용하는 방법이 있다
하지만 단지 print() 멤버함수를 특수화 하기 위해서 클래스 전체를 특수화 시키기 때문에 비효율적이다
C++에서는 특정 멤버함수를 특수화 시키기 위해서 클래스 전체를 특수화 하라고 요구하지 않는다
template <typename T>
class Storage
{
private:
T m_value{};
public:
Storage(T value) : m_value{ value } {}
void print()
{
std::cout << m_value << '\n';
}
};
template<>
void Storage<double>::print()
{
std::cout << "Double value: " << m_value << '\n';
}
이렇게 멤버 함수만 따로 특수화가 가능하다, 이때 클래스<타입>::함수명()으로 정의해야 한다
이러한 멤버함수 템플릿 특수화도 마찬가지로 암시적으로 inline이 아니기 때문에 헤더에 사용하기 위해서는 inline키워드를 사용해야 한다
일반적으로 템플릿 특수화 정의는 원본 클래스 아래에 정의하는 경우가 많다 (같은 헤더파일)
이렇게 되면 단일 헤더파일을 include하면 기본 템플릿과 특수화 템플릿을 둘 다 볼 수 있기 때문에 사용이 가능해진다
만약 특수화 버전이 특정 cpp 하나에서만 필요하다면 해당 cpp에서 특수화를 진행해도 상관은 없다, 이렇게 되면 다른 cpp에서는 전부 원본 버전을 사용하게 될 것이다
그리고 특정 .h를 만들어서 여기에 모든 템플릿 특수화를 구현하고 이 .h를 include해서 사용하는 방식은 좋지 않다 (include휴먼에러로 의도치 않은 동작이 발생할 수 있다)