25: 예외를 던지는 swap에 대한 지원도 생각해 보자
표준에서 기본적으로 제공하는 swap은 한 번 호출에 복사가 세 번 일어난다.
namespace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
타입에 따라서는 이런 사본이 정말 필요 없는 경우도 있다. 복사하면 손해를 보는 타입들 중 으뜸을 꼽는다면 아마도 다른 타입의 실제 데이터를 가리키는 포인터가 주성분인 타입일 것이다. 그런 기법이 바로 'pimpl 관용구'이다.
class WidgetImpl {
private:
int a, b, c; // 복사 비용이 높은 데이터들
vector<double> v;
};
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
*pImpl = *(rhs.pImpl);
}
private:
WidgetImpl* pImpl;
};
이렇게 만들어진 Widget 객체를 맞바꾼다면, pImpl 포인터만 살짝 바꾸는 것 말고는 실제로 할 일이 없다. 하지만 이런 사정을 표준 swap이 알 턱이 없다. 이럴 때는 일반적인 방법을 쓰지 말고 내부의 pImpl 포인터만 맞바꾸라고 알려 줘야 한다.
namespace std {
template<> // T가 Widget일 경우에 특수화
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.plImpl);
}
}
위의 코드는 아직 컴파일이 안 된다. 함수 시작 부분의 'template<>'는 이 함수가 std::swap의 완전 템플릿 특수화(total template specialization) 함수라는 것을 컴파일러에게 알려주는 부분이다. 함수 이름 뒤에 있는 '<Widget>'은 T가 Widget일 경우에 대한 특수화라는 사실을 알려 주는 부분이다. 다시 말해, 타입에 무관한 swap 템플릿이 Widget에 적용될 때는 위의 함수 구현을 사용해야 한다는 뜻이다.
그러나 위의 코드는 컴파일 되지 않는다. private 멤버인 pImpl 포인터에 접근하려고 하기 때문이다. 그래서 Widget 안에 swap이라는 public 멤버 함수를 선언하고 그 함수가 실제 맞바꾸기를 수행하도록 만든 후에, std::swap의 특수화 함수에게 그 멤버 함수를 호출하는 일을 맡긴다.
class Widget {
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
...
};
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
이제 컴파일도 되고, 기존의 STL 컨테이너와 일관성도 유지되는 착한 코드가 되었다.
그런데 Widget과 WidgetImpl가 클래스가 아니라 클래스 템플릿으로 만들어져 있어서, WidgetImpl에 저장된 데이터의 타입을 매개변수로 바꿀 수 있다면 어떻게 될까?
template<typename T>
class WidgetImpl {...};
template<typename T>
class Widget {...};
swap 멤버 함수를 Widget에 넣는 정도는 별로 어렵지 않지만, std::swap을 특수화하는 데는 힘들다.
namespace std {
template<typename T>
void swap<Widget<T>(Widget<T>& a, Widget<T>& b) // 에러!
{
a.swap(b);
}
}
지금 함수 템플릿(std::swap)을 부분적으로 특수화해 달라고 컴파일러에게 요청하는 것인데, C++는 클래스 템플릿에 대해서는 부분 특수화(partial specialization)를 허용하지만 함수 템플릿에 대해서는 허용하지 않도록 정해져 있다. 그러니 컴파일 불가다.
함수 템플릿을 '부분적으로 특수화'하고 싶을 때 흔히 취하는 방법은 그냥 오버로드 버전을 하나 추가하는 것이다.
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
일반적으로 함수 템플릿의 오버로딩은 해도 별 문제가 없지만, std는 조금 특별한 네임스페이스 이다. std 내의 템플릿에 대한 완전 특수화는 OK이지만, std에 새로운 템플릿을 추가하는 것은 OK가 아니다.
해결 방법은 멤버 swap을 호출하는 비멤버 swap을 선언해 놓되, 이 비멤버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 된다. 예를 들어, Widget 관련 기능이 전부 WidgetStuff 네임스페이스에 들어 있다고 가정하면 다음과 같이 만들라는 이야기이다.
namespace WidgetStuff {
template<typename T>
class Widget {...};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
이제는 어떤 코드가 두 Widget 객체에 대해 swap을 호출하더라도, 컴파일러는 C++의 이름 탐색 규칙에 의해 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아낸다.
이제 사용자의 눈으로 어떤 상황 하나를 놓고 이야기해 보자. 어떤 함수 템플릿을 만들고 있는데, 이 함수 템플릿은 실행 중에 swap을 써서 두 객체의 값을 맞바꾸어야 한다고 가정하자.
template<typename T>
void doSomthing(T& obj1, T& obj2)
{
swap(obj1, obj2);
}
이 부분에서 과연 어떤 swap을 호출해야 할까? 가능성은 세 가지 이다.
- std에 있는 일반형 버전: 무조건 존재한다.
- std의 일반형을 특수화한 버전: 있을 수도, 없을 수도 있다.
- T 타입 전용의 버전: 있거나 없거나 할 수 있으며, 어떤 네임스페이스 안에 있거나 없거나 할 수도 있다.
우리는 타입 T 전용 버전이 있으면 그것이 호출되도록 하고, 없으면 std의 일반형 버전이 호출되도록 만들고 싶다. 아래 코드가 정답이다.
template<typename T>
void doSomthing(T& obj1, T& obj2)
{
using std::swap;
swap(obj1, obj2);
}
컴파일러가 위의 swap 호출문을 만났을 때 하는 일은 현재의 상황에 딱 맞는 swap을 찾는 것이다. C++의 이름 탐색 규칙을 따라, 우선 전역 유효범위 혹은 타입 T와 동일한 네임스페이스 안에 T 전용의 swap이 있는지를 찾는다. T 전용 swap이 없으면 컴파일러는 그 다음 순서를 밟는데, 이 함수가 std::swap을 볼 수 있게 해 주는 using 선언(using declaration)이 함수 앞부분에 떡 하니 있기 때문에 std의 swap을 쓰게끔 결정할 수도 있다. 하지만 이런 상황이 되더라도 컴파일러는 std::swap의 T 전용 버전을 일반형 템플릿보다 더 우선적으로 선택하도록 정해져 있기 때문에, T에 대한 std::swap의 특수화 버전이 이미 준비되어 있으면 결국 그 특수화 버전이 쓰이게 된다.
멤버 버전의 swap은 절대로 예외를 던지지 않도록 만들어야 한다.
이것만은 잊지 말자!
- std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자. 이 멤버 swap은 예외를 던지지 않도록 만들어야 한다.
- 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공한다. 클래스(템플릿이 아닌)에 대해서는 std::swap도 특수화해 둔다.
- 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출하자.
- 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하다. 그러나 std에 어떤 것이라도 새로 '추가'하려고 들지는 말자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 27: 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2023.10.26 |
---|---|
[Effective C++] 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2023.10.23 |
[Effective C++] 24: 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (1) | 2023.10.21 |
[Effective C++] 23: 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (1) | 2023.10.20 |
[Effective C++] 22: 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2023.10.19 |