※ 특수화, 부분 특수화에 대한 이해가 선행되어야 하는 항목이다.
1. 함수 템플릿과 클래스 템플릿
2. C++ 템플릿 특수화/부분 특수화
3. C++ 함수 부분 특수화
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a); // a->temp 복사
a = b; // b->a 복사
b = temp; // temp->b 복사
}
}
다음은 표준 라이브러리제서 제공하는 swap 알고리즘이다.
우리가 알고있는 swap과 다르지 않은 것을 알 수 있다.
복사 생성자와 복사 대입 연산자를 통해 복사가 가능한 타입이기만 하면 어떤 타입의 객체든 맞바꾸기 동작을 수행한다.
swap 호출 1번에 복사가 3번이나 일어난다.
타입에 따라서는 이런 복사 과정이 필요 없는 경우도 있는데, 대표적으로 포인터가 있다.
// Widget의 실제 데이터를 나타내는 클래스
class WidgetImpl {
public:
...
private:
int a, b, c;
vector<double> v;
... // 복사 비용이 높은 수많은 데이터들...
};
// pImpl 관용구를 사용한 클래스
class Widget {
public:
Widget(const Widget& rhs);
// Widget을 복사하려면 WidgetImpl 객체를 복사해야 한다.
Widget& opertaor=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl *pImpl; // Widget의 실제 데이터를 가진 객체에 대한 포인터
};
Widget
객체를 우리가 직접 맞바꾼다면 pImpl
포인터만 바꾸기만 하면 된다. 그러나 이를 표준 알고리즘인 swap
에게 맡긴다면 Widget 객체를 3번 복사(+WidgetImpl도 3번 복사)할 것이다.
표준 swap이 Widget
객체를 swap할 때는 일반적인 방법이 아니라 내부의 pImpl 포인터만 맞바꾸도록 만들자!
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) // T가 Widget일 경우에 대해 std::swap 특수화
{
swap(a.pImpl, b.pImpl); // pImpl 포인터만 맞바꾸기
}
}
template<>
: 이 함수가 std::swap
의 완전 템플릿 특수화(total template specializaation) 함수라는 것을 컴파일러에게 알림
swap<Widget>
: T
가 Widget
일 경우에 대한 특수화임을 알림
참고
- 일반적으로 std 네임스페이스의 구성요소는 함부로 변경 불가
- but 프로그래머가 직접 만든 타입(ex.
Widget
)에 대해 표준 템플릿(ex.swap
)을 완전 특수화하는 것은 허용
그러나 위의 코드는 컴파일이 불가능하다. pImpl
포인터가 private 멤버이기 때문이다.
특수화 함수를 프렌드로 선언해도 되지만, 표준 템플릿의 규칙과 어긋나므로 일관성을 해치게 된다.
Widget
안에 실제 맞바꾸기를 수행하는 public 멤버 함수 swap
를 만들고 std::swap
의 특수화 함수가 그 멤버 함수를 호출한다.
class Widget {
public:
...
void swap(Widget& other)
{
using std::swap; // 이후 설명
swap(pImpl, other.pImpl); // 일반적인 표준 swap 사용하여 pImpl 포인터 맞바꾸기
}
...
};
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) // T가 Widget일 경우에 대해 std::swap 특수화
{
a.swap(b); // Widget의 swap 함수 호출
}
}
이렇게 하면 컴파일도 성공적이고, 기존 STL 컨테이너와의 일관성도 유지할 수 있다.
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
이전에 설명한 바와 동일하게 구현하면 되지 않을까 라고 생각할 것이다.
이전처럼 swap
멤버 함수를 Widget
에 넣는 것은 어렵지 않지만, swap
함수의 특수화가 불가능하다.
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) // Error.
{ a.swap(b); }
}
이렇게 Widget<T>
에 대해 특수화하려는 코드는 C++에서 불가능하다.
이는 즉 함수 템플릿(std::swap
)을 부분적으로 특수화해 달라고 컴파일러에게 요청한 것인데,
C++은 클래스 템플릿에 대한 부분 특수화는 허용하지만, 함수 템플릿에 대한 부분 특수화는 허용하지 않기 때문이다.
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) // std::swap 오버로드 시도.
{ a.swap(b); }
}
※ 위 코드는 유효하지 않다!!!
namespace std
- 일반적인 함수 템플릿은 오버로딩이 가능하지만, std는 특별한 네임스페이스이다.
- std 내의 템플릿에 대한 완전 특수화는 가능하지만, std에 새로운 템플릿을 추가하는 것은 금지하고 있다.
- 따라서 std의 영역을 침범해도 일단 컴파일은 성공하지만, 실행 결과 미정의 사항이다.
아무튼 우리의 목적은 일반적인 std::swap
이 아니라 우리가 만든 효율적인 템플릿 전용 버전 swap
을 쓰는 것이다.
따라서 std::swap
의 특수화나 오버로딩 버전이 아닌 비멤버 함수 swap
이 멤버 swap
을 호출하면 된다!
namespace WidgetStuff {
...
template<typename T>
class Widget { ... };
...
// 비멤버 swap
// std 네임스페이스와는 관련 X
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b); // 멤버 swap 호출
}
}
어떤 코드가 두 Widget
객체에 대해 swap
을 호출했을 때, 컴파일러는 C++의 이름 탐색 규칙인 인자 기반 탐색/쾨티그 탐색에 의해 WidgetStuff
네임스페이스의 swap
을 찾아낼 것이다.
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}
만일 3번이 있으면 3번을, 없으면 1번 버전을 호출하고 싶다면 아래 코드처럼 구현하면 된다.
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // std::swap을 이 함수 안으로 끌어올 수 있도록 함
...
swap(obj1, obj2); // T 타입 전용 swap 호출
...
}
컴파일러 실행 순서
T
와 동일한 네임스페이스의 T
전용 swap
을 찾음WidgetStuff
네임스페이스의 Widget
이라면 WidgetStuff
의 swap
을 찾음swap
이 없으면 using std::swap;
으로 인해 std::swap
을 사용하게 됨std::swap
의 T에 대한 특수화 버전이 있다면 특수화 버전 사용std::swap(obj1, opbj2); // 잘못된 방법
위의 코드는 std::swap
외에는 어떤 템플릿 특수화 버전들까지도 모두 사용하지 않도록 한다.
결국 딱 맞는 T
전용 버전이 다른 곳에 있을지도 모르는데 이를 완전히 무시하게 된다.
그래서 std::swap
을 완전 특수화하는 것이 중요하다. 한정자를 잘못 붙여도 std 내의 T
전용 swap
함수를 사용할 수 있기 때문이다.
만약 표준 swap이 비효율적이라면
swap
을 public 멤버 함수로 선언 (단, 예외를 던지면 안됨)swap
을 비멤버 swap
이 호출std::swap
의 특수화 버전 선언. 2번에서 만든 멤버 swap을 이 특수화 버전이 호출마지막으로, 사용자가 swap
을 호출할 때 swap
을 호출하는 함수가 std::swap
도 사용할 수 있도록 using
선언을 반드시 포함+네임스페이스 한정자 붙이지 않기
멤버 swap
은 절대 예외를 던져서는 안된다.
swap
을 응용하는 방법 중에 클래스(+클래스 템플릿)가 강력한 예외 안정성 보장을 제공하도록 도움을 주는 방법이 있기 때문이다....?
무슨 말인지 모르겠다. 나중에 정리,,