항목 25 : 예외를 던지지 않는 swap에 대한 지원도 생각해 보자
swap는 자기대입 현상의 가능성에 대처하기 위한 대표적인 메커니즘으로서 널리 사랑받아 왔습니다.
두 객체의 값을 '맞바꾸기'한다는 것은 각자의 값을 상대방에게 주는 동작입니다.
표준 swap가 동작시 복사가 세 번 일어납니다. 이런 복사가 일어날시 손해를 보는 타입들 중 으뜸을 꼽는다면 아마도 다른 타입의 실제 데이터를 가리키는 포인터가 주성분인 타입일 것입니다.
이러한 개념을 쓰고 있는 기법이 바로 pImpl입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class WidgetImpl {
public :
...
private :
int a, b, c;
std::vector<double> v;
...
};
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) // Widget을 복사하기 위해, 자신의 WidgetImpl 객체를 복사합니다.
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private :
WidgetImpl * pImpl; // Widget의 실제 데이터를 가진 객체에 대한 포인터
};
Colored by Color Scripter
|
이렇게 만들어진 Widget 객체를 우리가 직접 맞바꾼다면, pImpl 포인터만 살짝 바꾸는 것 말고는 실제로 할 일이 없습니다. 그래서 조금 손을 보고 싶습니다. std::swap에다가 뭔가를 알려 주는거죠. Widget 객체를 맞바꿀 떄는 내부의 pImpl 포인터만 맞바꾸라고 말입니다. 기본 아이디어만 코드로 보여드리겠습니다. 아직 컴파일은 되지 않습니다.
1
2
3
4
5
6
7
8
|
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) // 이 코드는 T가 Widget일 경우에 대해 std::swap을 특수화한 것입니다.
{
swap(a.pImpl, b.pImpl);
}
}
Colored by Color Scripter
|
함수 시작 부분에 잇는 'template<>'가 std::swap의 완전 템플릿 특수화 함수라는 것을 컴파일러에게 알려 주는 부분입니다. 일반적으로 std 네임스페이스의 구성요소는 함부로 변경하거나 할 수 없지만, 프로그래머가 직접 만든 타입에 대해 표준 템플릿을 완전 특수화하는 것은 허용이 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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);
}
}
Colored by Color Scripter
|
이렇게 작성하면 컴파일될 뿐만 아니라, 기존의 STL 컨테이너와 일관성도 유지되는 착한 코드가 되었습니다.
함수 템플릿을 부분적으로 특수화하고 싶을수도 있습니다. 하지만 C++는 클래스 템플릿에 대해서는 부분 특수화를 허용하지만 함수 템플릿에 대해서는 허용하지 않도록 정해져 있습니다.
함수 템플릿을 '부분적으로 특수화'하고 싶을 때는 오버로드 버전을 하나 추가하면 됩니다.
일반적으로 함수 템플릿의 오버로딩은 해도 별 문제가 없지만, std는 조금 특별한 네임스페이스이기 때문에 이 네임스페이스에 대한 규칙도 다소 특별합니다. std 내의 템플릿에 대한 완전 특수화는 OK이지만, std에 새로운 템플릿을 추가하는 것은 OK가 아닙니다.
우리는 swap을 호출해서 우리만의 효율 좋은 '템플릿 전용 버전'을 쓸 수 있으면 좋겠습니다. 이를 위한 방법은 간단합니다. 멤버 swap를 호출하는 비멤버 swap를 선언해 놓되, 이 비멤버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 됩니다.
정리를 해보겠습니다.
첫째, 표준에서 제공하는 swap이 여러분의 클래스 및 클래스 템플릿에 대해 납득할 만한 효율을 보이면, 그냥 아무것도 하지 마세요
둘째, 표준 swap의 효율이 기대한 만큼 충분하지 않다면, 다음과 같이 하십시오.
1. 여러분의 타입으로 만들어진 두 객체의 값을 바꾸는 함수를 swap라는 이름으로 만들고, 이것을 public 멤버 함수로 두십시오. 이 함수는 절대로 예외를 던져선 안 됩니다.
2. 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap을 만들어 넣습니다. 그리고 1번에서 만든 swap 멤버 함수를 이 비멤버 함수가 호출하도록 만듭니다.
3. 새로운 클래스를 만들고 있다면, 그 클래스에 대한 std::swap의 특수화 버전을 준비해 둡니다. 그리고 이 특수화 버전에서도 swap 멤버 함수를 호출하도록 만듭니다.
셋째, 사용자 입장에서 swap을 호출할 때, swap을 호출하는 함수가 std::swap을 볼 수 있도록 using 선언을 반드시 포함시킵니다. 그 다음에 swap을 호출하되, 네임스페이스 한정자를 붙이지 않도록 하십시오.
꼭 잊지 말아야 할 것!
1. std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공합시다. 이 멤버 swap은 예외를 던지지 않도록 만듭시다.
2. 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공합니다. 클래스(템플릿이 아닌)에 대해서는, std::swap도 특수화해 둡시다.
3. 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출합시다.
4. 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능합니다. 그러나 std에 어떤 것이라도 새로 '추가'하려고 들지는 마십시오.
'언어 > C++' 카테고리의 다른 글
[Effective C++] 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2020.01.27 |
---|---|
[Effective C++] 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2020.01.26 |
[Effective C++] 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2020.01.24 |
[Effective C++] 멤버 함수보다는 비멤버 비프렌드 함수와 더 까가워지자 (0) | 2020.01.23 |
[Effective C++] 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2020.01.22 |