[Effective C++] 25. 예외를 던지지 않는 swap에 대한 지원도 생각해 보자
std의 표준 swap
namespace std
{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
위에서 보는 것처럼 표준 swap은 복사 생성자와 복사 대입 연산자를 통해 이루어지는데, 복사만 지원하는 타입이라면 어떤 객체든 맞바꾸기 동작을 수행해주며 타입에 상관없이 1번 호출에 3번 복사가 일어난다. (a에서 temp, b에서 a, temp에서 b)
표준 swap의 단점
위에서 말한 것처럼 표준 swap은 1번 호출에 3번 복사가 이루어지기 때문에 다른 타입의 실제 데이터를 가리키는 포인터가 주성분 타입일 경우 복사 시 손해를 보게 된다.
이런 개념을 설계의 미학으로 끌어올려서 사용하는 기법이 pumpl 관용구이다.
class WidgetImpl
{
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은 Widget객체를 3개 복사하고 거기에 WidgetImpl객체도 세 개 복사해서 6번의 복사를 하게 된다.
따라서 Widget객체를 맞바꿀 때는 pImpl포인터만 맞바꾸라고 std::swap에 알려준다면 위의 비효율적인 문제가 개선된다.
즉, std::swap을 Widget에 대해 특수화하는 것이다.
완전 템플릿 특수화
namespace std
{
template <>
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl); // private 멤버에 접근 불가하므로 에러
}
}
위의 코드는 완전 특수화가 적용되었지만 pImpl에 접근할 수 없으므로 에러가 발생한다.
또, 일반적으로 std 네임스페이스의 구성요소는 함부로 변경 불가능하지만 사용자가 직접 만든 타입에 대해 표준 템플릿을 완전 특수화하는 것은 가능하다.
결국 private 멤버에 접근이 불가능한 게 문제이므로 해결방법은 Widget의 멤버 함수 swap을 만들고 std::swap의 특수화 함수에서 멤버 함수를 호출하는 것으로 문제를 해결할 수 있다.
class Widget
{
public:
void swap(Widget& other) // 아래 완전특수화된 std::swap에서 멤버함수를 호출하기전 미리 선언해두자.
{
using std::swap;
swap(pImpl, other.pImpl);
}
};
namespace std
{
template <>
void swap<Widget>(widget& a, Widget& b)
{
a.swap(b); // widget의 멤버함수를 호출한다. 그대신 멤버함수의 swap함수를 신규로 미리 선언해둬야한다.
}
}
클래스 템플릿 타입 부분 특수화
하지만 만약 Widget과 WidgetImpl이 클래스가 아니라 클래스 템플릿으로 만들어져 있다면 어떻게 될까?
C++은 클래스 템플릿에 대해서는 부분 특수화를 허용하지만 함수 템플릿(std::swap)에 대해서는 허용하지 않도록 정해져 있다. 그리고 std에서는 특수화 제외하고는 아무것도 변경할 수 없는 규칙이 있다.
즉, 아래와 같은 코드는 에러를 뱉는다는 말이다.
namespace std
{
template<typename T>
void swap<Widget<T>> (Widget<T>& a, Widget<T>& b) //ERROR!!!
{
a.swap(b);
}
}
이 해결법은 간단한데, 멤버 swap을 호출하는 비멤버 swap을 선언해 놓되, 이 비멤버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 된다.
예를 들어 Widget관련 기능이 WidgetStuff 네임스페이스에 들어가 있다고 가정하면 아래와 같은 방식으로 만들면 된다.
namespace WidgetStuff
{
template<typename T>
class Widget {...};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //비멤버 swap이며, std 네임스페이스에 포함되면안됨
{
a.swap(b);
}
}
그러면 어떤 코드가 두 Widget객체에 대해 swap을 호출하더라도, 컴파일러는 C++의 이름 탐색 규칙(인자 기반 탐색 or 쾨니크 탐색)에 따라 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아내서 사용한다.
이 방법은 클래스 템플릿뿐만 아니라 클래스에 대해서도 잘 통하므로 swap을 특수화하고 싶다면 클래스와 동일한 네임스페이스 안에 비멤버 버전의 swap을 만들어 넣고, std::swap의 특수화 버전도 준비해 두어야 한다.
swap 호출 순서 제어
1. T타입 전용 swap (전역 유효 범위 혹은 동일 네임스페이스 안에 있는 swap부터 찾아낸다.)
2. std::swap의 특수화 버전을 호출한다.
3. std::swap 표준 버전을 호출한다.
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // std::swap을 이 함수 안으로 끌어올 수 있도록 함
swap(obj1, obj2); // T타입 전용의 swap을 호출한다.
}
여기서 유의할 점은 멤버 swap은 절대 예외를 던지면 안 된다는 점이다. 왜냐면 swap을 쓸모 있게 응용하는 방법 들 중에 클래스(및 클래스 템플릿)가 강력한 예외 안정성 보장을 제공하도록 도움을 주는 방법의 전제기 때문이다.
요약
- std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자. 그리고 이 멤버 swap은 예외를 던지지 않도록 만들어야 한다.
- 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공하자. 클래스(템플릿이 아닌)에 대해서는, std::swap도 특수화해두자.
- 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어준 후에 네임스페이스 한정 없이 swap을 호출하자.
- 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하다. 그러나 std에 어떤 것이라도 새로 추가하려고 들지는 마라.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2022.06.18 |
---|---|
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2022.06.18 |
[Effective C++] 24. 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2022.06.12 |
[Effective C++] 23. 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2022.06.12 |
[Effective C++] 22. 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2022.06.12 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자
2022.06.18 -
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자
2022.06.18 -
[Effective C++] 24. 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자
[Effective C++] 24. 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자
2022.06.12 -
[Effective C++] 23. 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자
[Effective C++] 23. 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자
2022.06.12