- std::swap이 정의한 타입에 대해 느리게 동작할 여지가 있다면 swap을 멤버 함수로 제공해라.
이 멤버 swap은 예외를 던지지 않도록 해야 한다. - 멤버 swap을 제공했다면, 이 멤버를 호출하는 비멤버 swap도 제공해라. 클래스에 대해서는 std:swap도 특수화해야 한다.
- 사용자 입장에서 swap을 호출할 때, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출하라.
- 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하다. 그러나, std에 어떤 것이라도 추가하려 하지 마라.
swap 지원 & 예외 처리
swap은 예외 안전성 프로그래밍에 없어선 안되는 함수이다.
자기 대입의 가능성에 대처하기 위한 대표적인 메커니즘으로 널리 알려져 있다.
따라서 swap함수를 제대로 구현하는 것이 중요해졌다.
swap은 두 객체의 값을 맞바꾸는 함수이다.
namepace std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
표준에서 기본으로 제공하는 swap 함수이다.
코드를 보면 복사만 제대로 지원하는 타입이기만 하면 어떤 타입의 객체이든 맞바꾸기 동작을 수행한다.
하지만, 표준 swap의 동작을 보면 a에서 temp로, b에서 a로, temp에서 b로 복사가 세 번 일어난다.
복사를 수행할 때, 객체에 대한 복사 비용을 고려해야 한다.
이러한 부분은 pimpl (pointer to implementation) 기법 으로 해결할 수 있다.
class WidgetImpl { // 복사 비용이 높다.
public:
...
private:
int a, b, c;
std::vector<double> v;
...
};
class Widget { // 포인터만 복사 = 복사 비용 적음
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl *pImpl;
};
Widget 객체를 직접 맞바꾼다면, pImpl 포인터만 살짝 바꾸는 것 말고는 할 일이 없다.
하지만, 표준 swap 알고리즘은 이러한 사실을 알 수 없다.
그래서 이러한 문제를 해결하기 위해 std::swap에 Widget 객체를 맞바꾸는 방법을 알려주고 싶다.
이것을 특수화(specialize)라고 한다.
특수화
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) // T가 Widget일 경우 특수화
{
swap(a.pImpl, b.pImpl); // 포인터만 swap
}
}
함수 시작부분에 template<>이란 부분은 이 함수가 std::swap의 완전 템플릿 특수화함수라는 것을 컴파일러에게 알려주는 부분이다.
그리고 함수 이름 뒤에 <Widget>은 T가 Widget일 경우에 대한 특수화라는 사실을 알려주는 부분이다.
즉, Widget에 대해 swap이 이루어지면 위의 함수를 사용해야 한다는 뜻이다.
일반적으로 std 네임스페이스의 구성요소를 함수로 변경할 수 없지만, 프로그래머가 직접 만든 타입에 대해 표준 템플릿을 완전 특수화하는 것은 허용된다.
위의 코드는 아직 컴파일이 되지 않는다. a, b에 있는 pImpl 포인터가 private이기 때문이다.
특수화 함수를 프렌드로 선언할 수 있지만, 이렇게 하면 표준 템플릿들에 쓰인 규칙과 어긋 나므로 지양해야 한다.
그래서 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 컨테이너와 일관성도 유지하는 코드가 됐다.
public 멤버 함수 번전의 swap과 이 멤버 함수를 호출하는 std::swap의 특수화 함수 모두 지원하고 있다.
그런데 이제 Widget과 WidgetImpl이 클래스가 아니라 클래스 템플릿으로 만들어져 있어서, WidgetImpl에 저장된 데이터의 타입을 매개변수로 바꿀 수 있다면 위의 방법은 지원이 되지 않는다.
template<typname T>
class WidgetImpl { ... };
template<typname T>
class Widget { ... };
namespace std {
template<typname T>
vodi swap<Widget<T>>(Widget<T>& a, Widget<T>& b) // Error
{ a.swap(b); }
}
swap을 멤버 함수로 Widget에 넣는 것은 가능하지만, std::swap을 특수화하는 부분에서 문제가 발생한다.
지금은 swap을 부분적으로 특수화해 달라고 요청한 것인데, C++는 클래스 템플릿에 대해서는 부분 특수화를 허용하지만 함수 템플릿에 대해서는 허용하지 않는다.
함수 템플릿을 부분 특수화하고 싶을 때 흔히 취하는 방법은 그냥 오버로드 버전을 하나 추가하는 것이다.
namespcae std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) // swap 뒤에 <...>이 없음
{ a.swap(b) }
}
일반적으로 함수 템플릿의 오버로딩은 문제가 되지 않지만, std는 조금 특별한 네임스페이스이기 때문에 이 네임스페이스에 대한 규칙도 특별하다.
std 내의 템플릿에 대한 완전 특수화는 가능하지만, std에 새로운 템플릿을 추가하는 것은 불가능하다.
만약, std에 무엇인가를 추가한다면 컴파일은 되지만 실행결과가 미정의 사항이 된다. 그러니 절대 std에 아무것도 추가하지 마라.
해결법
swap을 호출해서 효율 좋은 템플릿 버전을 쓰고 싶다.
멤버 swap을 호출하는 비멤버 swap을 선언하고 이 비멤버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 된다.
namespace WidgetStuff {
...
template<typename T>
class Widget { ... }; // swap 멤버 함수 포함
...
template<typenae T>
void swap(Widget& a, Widget& b)
{
a.swap(b);
}
}
이제는 두 Widget 객체에 대해 swap을 호출하더라도, 컴파일러는 C++의 이름 탐색 규칙에 의해 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아낸다.
이 방법은 클래스 템플릿뿐만 아니라 클래스에 대해서도 적용된다.
제작하는 클래스에 대해 std::swap을 특수화해야 할 이유가 생긴다면 전용 swap을 동일한 네임스페이스 안에 비멤버 버전의 swap을 만들어 놓고, 그와 동시에 std::swap의 특수화 버전도 준비해 두어야 한다.
그런데, 네임스페이스를 안 쓰고 있어도 유효하다. 하지만 전역 네임스페이스를 굳이 쓸 이유는 없다.
네임스페이스를 사용하는 것을 권장한다.
사용 관점
swap을 사용하는 상황을 살펴보자.
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}
이 부분에서 호출될 수 있는 swap은 세 가지이다.
- std에 있는 일반형 swap (무조건 존재)
- std의 swap을 특수화한 swap (존재할 수도 아닐 수도)
- T 타입 전용 swap (존재할 수도 아닐 수도)
만약, T타입 전용 swap이 있으면 호출하고 없다면 std에 있는 swap을 사용하게 만들고 싶다면 다음과 같이 하면 된다.
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // std::swap을 이 함수 안으로 끌어옴
...
swap(obj1, obj2);
...
}
컴파일러가 위의 swap 호출문을 만났을 때 현재 상황에 맞는 swap을 찾을 것이다.
C++ 이름 탐색 규칙을 따라, 전역 유효범위 혹은 타입 T와 동일한 네임스페이스 안에 T전용 swap이 있는지를 찾는다.
T전용 swap이 없다면 컴파일러는 다음 순서를 찾을 것인데 이때 using 선언을 보고 std::swap을 쓰도록 결정할 것이다.
또한, std::swap의 T전용 버전을 우선적으로 선택할 것이다.
이때 주의할 점은 한정자를 사용하지 않는 것이다.
std::swap(obj1, obj2); // std::swap만 사용하게 됨
이렇게 std::라는 한정자를 붙이면 무조건 std내의 swap을 찾을 것이다.
이러한 부분이 std::swap을 완전 특수화하는 게 중요한 이유이다.
정리
- 표준에서 제공하는 swap이 클래스 및 클래스 템플릿에 대해 납득할만한 효율을 보이면 아무것도 하지 마라.
- 표준 swap의 효율이 문제가 있다면 다음과 같이 해라.
- 제작 중인 클래스에 대한 올바른 swap 함수를 만들고, 이것을 public으로 두어라.
- 클래스 혹은 템플릿이 있는 네임스페이스에 비멤버 swap을 만들고 이것이 1번에서 만든 멤버 함수를 호출하게 해라.
- 제작 중인 클래스에 대한 std::swap의 특수화 버전을 준비해라. 이 특수화 버전에서도 멤버 swap을 호출하게 하라.
- 사용자 입장에서 swap을 호출할 때, swap을 호출하는 함수가 std::swap을 볼 수 있게 using 선언을 반드시 포함해라.
멤버 버전의 swap은 절대로 예외를 던지면 안 된다.
그 이유는 swap은 클래스의 강력한 예외 안전성 보장을 제공하는 함수로 사용되기 때문이다.