
부분 템플릿 특수화
다음과 같은 코드가 있다고 가정해보자
template<typename T, int size>
class StaticArr
{
public:
T* GetArr()
{
return arr;
}
const T& operator[](int index) const
{
return arr[index];
}
private:
T arr[size]{};
};
이 StaticArr클래스는 두개의 템플릿 매개변수를 가진다, 하나는 타입 매개변수인 T이고 나머지는 비타입 매개변수인 size이다
따라서 StaticArr<int, 4>, StaticArr<double, 10>은 컴파일러에 의해 완전히 독립적인 두 개의 클래스로 인스턴스화 된다
그렇다면 이 StaticArr의 배열 전체를 출력하는 함수를 일반함수로 만들어보자
template<typename T, int size>
void printArr(const StaticArr<T, size>& InArr)
{
for (int count = 0; count < size; ++count)
{
std::cout << InArr[count] << ' ';
}
}
int main()
{
StaticArr<int, 4> intArr{};
intArr[0] = 1;
intArr[1] = 2;
intArr[2] = 3;
intArr[3] = 4;
printArr(intArr); //1 2 3 4
}
하지만 만약 StaticArr의 배열이 char타입이라면 어떨까? 중간중간에 공백이 들어가 가독성이 떨어질 것이다
int main()
{
StaticArr<char, 6> charArr{};
constexpr std::string_view temp{ "Kelvin" };
std::copy_n(temp.begin(), temp.size(), charArr.GetArr());
printArr(charArr); //K e l v i n
}
이렇게 특정 타입에 대해 다른 동작을 하는 템플릿을 구현하기 위해 우리는 full template specialization이라는 방법을 생각해낼 수 있다
template<>
void printArr(const StaticArr<char, 6>& InArr)
{
for (int count = 0; count < 6; ++count)
{
std::cout << InArr[count];
}
}
단 이렇게 full template specialization을 하게 되면 모든 템플릿 매개변수를 명시해줘야 하기 때문에 우리가 구현한 template specialization은 반드시 타입 템플릿 매개변수는 char이고 비 타입 템플릿 매개변수는 6일때만 동작한다
따라서 StaticArr<char, 10>은 적용할 수 없다는 의미이다
그렇다면 하나하나 전부 다 full template speicalization을 하면서 구현해야 할까? 이럴때 바로 partial template specialization을 활용한다
부분 템플릿 특수화는 전체 템플릿 특수화와 다르게 템플릿 매개변수중 일부만 정의하고 나머지는 여전히 템플릿 매개변수로 남겨놓는 방식으로 클래스를 특수화 할 수 있게 해준다 (함수는 불가능, 클래스만 가능)
일반 함수와 같은 경우에는 부분 템플릿 특수화를 사용할 수 없기 때문에 템플릿 오버로딩을 사용하여 구현한다
template<typename T, int size>
void printArr(const StaticArr<T, size>& InArr)
{
for (int count = 0; count < size; ++count)
{
std::cout << InArr[count] << ' ';
}
}
//템플릿 오버로딩
template<int size>
void printArr(const StaticArr<char, size>& InArr)
{
for (int count = 0; count < size; ++count)
{
std::cout << InArr[count];
}
}
하지만 멤버 함수와 같은 경우에는 조금 이야기가 다르다
template<typename T, int size>
class StaticArr
{
public:
T* GetArr()
{
return arr;
}
const T& operator[](int index) const
{
return arr[index];
}
T& operator[](int index)
{
return arr[index];
}
void printArr() const;
private:
T arr[size]{};
};
template<typename T, int size>
void StaticArr<T, size>::printArr() const
{
for (int i = 0; i < size; ++i)
{
std::cout << arr[i] << " ";
}
}
이 상황에서 double일때만 특수화를 하고싶으면 어떻게 할까? 함수는 부분 템플릿 특수화가 불가능하다
template<int size>
void StaticArr<double, size>::printArr() const
{
for (int i = 0; i < size; ++i)
{
std::cout << arr[i] << " ";
}
}
//error!
해결책 중 하나는 클래스 전체를 부분 템플릿 특수화 하고 멤버 함수를 호출하는 방법이다
template<typename T, int size>
class StaticArr
{
public:
T* GetArr()
{
return arr;
}
const T& operator[](int index) const
{
return arr[index];
}
T& operator[](int index)
{
return arr[index];
}
void printArr() const;
private:
T arr[size]{};
};
template<typename T, int size>
void StaticArr<T, size>::printArr() const
{
for (int i = 0; i < size; ++i)
{
std::cout << arr[i] << " ";
}
}
//클래슽 템플릿 부분 특수화
template<int size>
class StaticArr<double, size> //부분 특수화
{
public:
double* GetArr()
{
return arr;
}
const double& operator[](int index) const
{
return arr[index];
}
double& operator[](int index)
{
return arr[index];
}
void printArr() const;
private:
double arr[size]{};
};
template<int size>
void StaticArr<double, size>::printArr() const
{
for (int i = 0; i < size; ++i)
{
std::cout << arr[i] << " ";
}
}
이제 StaticArr<double, size>일때 특수화가 가능해졌다
하지만 기본 템플릿 클래스의 대부분의 코드를 복사 붙혀넣기 해야하기 때문에 권장되지 않는 방법이다, 가독성도 떨어지고 코드 유지보수성이 낮아진다
따라서 두번재는 상속을 이용한 코드 재사용이다
template<typename T, int size>
class StaticArrBase
{
public:
T* GetArr()
{
return arr;
}
const T& operator[](int index) const
{
return arr[index];
}
T& operator[](int index)
{
return arr[index];
}
void printArr() const;
protected:
T arr[size]{};
};
template<typename T, int size>
class StaticArr : public StaticArrBase<T, size>
{
};
template<int size>
class StaticArr<double, size> : public StaticArrBase<double, size>
{
public:
void printArr() const
{
for (int i = 0; i < size; ++i)
{
std::cout << this->arr[i] << " ";
}
}
};
상속을 받지 않았다면
template<typename T, int size>
class StaticArrBase
{
public:
T* GetArr()
{
return arr;
}
const T& operator[](int index) const
{
return arr[index];
}
T& operator[](int index)
{
return arr[index];
}
void printArr() const;
protected:
T arr[size]{};
};
template<int size>
class StaticArrBase<double, size>
{
public:
T* GetArr()
{
return arr;
}
const T& operator[](int index) const
{
return arr[index];
}
T& operator[](int index)
{
return arr[index];
}
void printArr() const;
protected:
T arr[size]{};
};
이렇게 길어졌을 코드가 상속으로 인해 간단해졌다
코드 중복을 피해 가독성이 상승하고 코드 유지보수성이 좋아졌다 (StaticArrBase만 수정하면 상속받는 나머지도 같이 수정됨)
포인터에 대한 부분 템플릿 특수화
다음과 같은 함수 템플릿 완전 특수화 코드가 있다고 생각해보자
template<typename T>
class Foo
{
public:
Foo(T value) : fooValue{ value }
{
}
void print()
{
std::cout << fooValue << '\n';
}
private:
T fooValue;
};
//멤버함수 템플릿 완전 특수화
template<>
void Foo<double>::print()
{
std::cout << "Double" << fooValue << '\n';
}
int main()
{
Foo<int> intFoo(30);
Foo<double> doubleFoo(1.5f);
intFoo.print();
doubleFoo.print();
}
아주 정상적으로 double일때 특수화 되어 30과 Double 1.5가 나오게 된다
그렇다면 만약 T가 포인터면 어떤 상황이 발생할까?
int main()
{
double d{ 1.5 };
double* ptrD{ &d };
Foo<double*> fooPtr{ ptrD };
fooPtr.print();
}
값은 주소값이 나오게 된다
왜냐하면 Foo<double> fooPtr에는 ptrD가 들어갔고 T가 double이기 때문에 포인터값을 출력하기 때문이다
그렇다면 실제 값을 출력하려면 어떻게 해야할까?
첫번째로 포인터 타입에 대한 완전 특수화를 추가하는 방법이다
template<typename T>
class Foo
{
public:
Foo(T value) : fooValue{ value }
{
}
void print()
{
std::cout << fooValue << '\n';
}
private:
T fooValue;
};
template<>
void Foo<double>::print()
{
std::cout << "Double" << fooValue << '\n';
}
//double*로 멤버 함수 템플릿 완전 특수화
template<>
void Foo<double*>::print()
{
std::cout << "Double Ptr" << *fooValue << '\n';
}
int main()
{
double d{ 1.5 };
double* ptrD{ &d };
Foo<double*> fooPtr{ ptrD };
fooPtr.print();
}
하지만 이는 double일때만 동작한다, 다른 int나 char*일때는 동작하지 않는다
따라서 특정 타입의 포인터에 대한 특수화가 아닌 포인터에 대한 특수화가 필요하다
그렇다면 그냥 template< typename T >하고 void Foo<T*>::print()로 하면 어떻게 될까?
template<typename T>
class Foo
{
public:
Foo(T value) : fooValue{ value }
{
}
void print()
{
std::cout << fooValue << '\n';
}
private:
T fooValue;
};
template<typename T>
void Foo<T*>::print()
{
std::cout << *fooValue;
}
해당 코드는 컴파일이 불가능하다, 왜냐하면 멤버함수에 대한 부분 템플릿 특수화 이기 때문이다 (전체 템플릿 특수화는 template<>으로 비어있어야 함)
따라서 클래스 자체를 클래스 템플릿 부분 특수화를 시켜주고 멤버 함수를 정의해야 한다
template<typename T>
class Foo
{
public:
Foo(T value) : fooValue{ value }
{
}
void print();
private:
T fooValue;
};
//클래스 템플릿 부분 특수화
template<typename T>
class Foo<T*>
{
public:
Foo(T* value) : fooValue{ value }
{
}
void print();
private:
T* fooValue;
};
//부분 특수화된 클래스의 멤버함수 정의
template<typename T>
void Foo<T*>::print()
{
std::cout << *fooValue;
}
여기서의 void Foo<T*>::print()는 부분 특수화된 멤버함수가 아닌 부분 특수화된 클래스의 멤버함수 정의이기 때문에 컴파일 에러가 발생하지 않는다
단 여기서 주의할점은 class Foo<T*>로 부분 특수화 한 클래스는 내부 멤버데이터로 raw pointer를 가진다는 점이다, 따라서 해당 raw pointer (위에서는 fooValue)가 가리키는 객체가 소멸된다면 이 멤버 raw pointer는 dangling pointer가 되기 때문에 주의해야 한다
따라서 T*로 특수화한 템플릿 클래스의 멤버 변수는 raw pointer가 아닌 스마트 포인터를 사용하는걸 권장한다
template<typename T>
class Foo
{
public:
Foo(T value) : fooValue{ value }
{
}
void print();
private:
T fooValue;
};
template<typename T>
class Foo<T*>
{
public:
Foo(T* value) : fooValue{ std::make_unique<T>(value ? *value : T{}) }
{
}
void print();
private:
std::unique_ptr<T> fooValue; //스마트포인터 사용
};
int main()
{
double d{1.2};
Foo f1{d};
d.print(); //기본 템플릿 함수 호출
Foo f2{&d};
d.print(); //부분 특수화 템플릿의 멤버함수 호출
}
이렇게 되면 *value로 역참조 후 실제값인 1.2를 가져오고 std::make_unique< T >를 통해 T타입의 객체를 동적할당한다, 이때 T타입의 복사 생성자를 호출하여 1.2라는 값을 복사해 넣는다
결국 Foo클래스 내부에서 std::unique_ptr<>이라는 스마트포인터 객체인 fooValue가 d를 가리키기 때문에 d가 소멸되어도 자신만의 복사본을 가지고 있어서 dangling pointer가 발생하지 않는다
이제 main()이 종료되고 f2가 소멸되면 Foo<T*>클래스 내부의 std::unique_ptr< T >인 fooValue도 소멸되어 자동으로 동적 할당한 메모리가 해제되어 누수가 발생하지 않게 된다