KoreanFoodie's Study
Effective C++ | 항목 25 : 예외를 던지지 않는 swap 에 대한 지원도 생각해 보자 본문
Effective C++ | 항목 25 : 예외를 던지지 않는 swap 에 대한 지원도 생각해 보자
GoldGiver 2022. 10. 25. 16:15
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 25 : 예외를 던지지 않는 swap 에 대한 지원도 생각해 보자
핵심 :
1. std::swap 이 사용자 정의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자. 이 멤버 swap 은 예외를 던지지 않도록 만들자.
2. 멤버 swap 을 제공했으면, 이 멤버를 호출하는 비멤버 swap 도 제공하자. 클래스(템플릿이 아닌)에 대해서는, std::swap 도 특수화해 두자.
3. 사용자 입장에서 swap 을 호출할 때는, std::swap 에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap 을 호출하자.
4. 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하다. 그러나 std 에 어떤 것이라도 새로 '추가' 하려고 들지는 말자.
먼저 표준 라이브러리에서 제공하는 기본 swap 알고리즘 구현을 보자.
namespace std
{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
위의 코드는 복사 생성자 및 복대 대입 연산자를 호출한다.
손해를 줄이기 위해, 포인터 복사를 응용한 pimpl 기법을 보자(pointer to implementation).
class WidgetImpl
{
private:
int a, b, c;
std::vector<double> v;
...
};
class Widget
{
public:
Widget(const Widget& rhs);
// Widget 복사를 위해 포인터를 복사
Widget& operator=(const Widget& rhs)
{
*pImpl = *(rhs.pImpl);
}
private:
// Widget 의 실제 데이터를 가진 객체에 대한 포인터
WidgetImpl *pImpl;
};
std::swap 을 특수화하면, 위의 WidgetImpl 을 이용한 효율적 swap 을 구현할 수 있다.
namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl);
}
}
하지만 위 코드는 컴파일이 안된다. 왜냐하면 pImpl 이 private 이기 때문이다. Widget 클래스를 다음과 같이 조금 고쳐보자.
class Widget
{
public:
void swap(Widget& other)
{
// 해당 선언문은 왜 필요할까? (조금 이따가 공개...)
using std::swap;
swap(pImpl, ohter.pImpl);
}
...
private:
WidgetImpl *pImpl;
};
// swap 특수화 버전도 살짝 수정한다
namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
이제 swap 이 정상적으로 작동한다!
Widget 과 WidgetImpl 가 클래스가 아니라 클래스 템플릿으로 되어 있어서, WidgetImpl 에 저장된 데이터의 타입을 매개변수로 바꿀 수 있다면 어떻게 될까?
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
namespace std
{
// 에러! 적법하지 않은 코드!
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
C++ 는 클래스 템플릿에 대해서는 부분 특수화(partial specialization) 을 허용하지만 함수 템플릿에 대해서는 허용하지 않도록 정해져 있다. 따라서 컴파일이 되지 않는다!
함수 템플릿을 '부분적으로 특수화' 하고 싶을 때 흔히 취하는 방식은 다음과 같이 오버로드 버전을 하나 추가하는 것이다.
namespace std
{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
일반적으로 함수 템플릿의 오버로딩은 별 문제가 없다. 하지만 std 는 조금 특별해서, std 내의 템플릿에 대한 완전 특수화는 OK 지만, 새로운 템플릿을 추가하는 것은 금지되어 있다. 컴파일은 되지만 미정의 동작이 발생하므로, 절대 추가하지 말자!
결론은, 멤버 swap 을 호출하는 비멤버 swap 을 선언해 놓되, 이 비멤버 함수를 std::swap 의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 된다. 예를 한 번 보자.
namespace WidgetStuff
{
template<typename T>
class Widget { ... };
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
이제는 어떤 코드가 두 Widget 객체에 대해 swap 을 호출하더라도, 컴파일러는 C++ 의 탐색 규칙(인자 기반 탐색 : argument-dependent lookup) 혹은 쾨니그 탐색(Koenig lookup) 에 의해 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아낸다.
그렇다면 다음 코드에서는 어떤 swap 이 호출될까?
template<typename T>
void doSomething(T& obj1, T& obj2)
{
// std::swap 을 이 함수 안으로 끌어올 수 있도록 만드는 문장
using std::swap;
...
// T 타입 전용의 swap 을 호출한다
swap(obj1, obj2);
}
3 가지 가능성이 있다.
- std 에 있는 일반형 버전 : 이건 확실히 있다
- std 의 일반형을 특수화한 버전 : 이건 있을 수도, 없을 수도 있다
- T 타입 전용의 버전 : 있을 수도, 없을 수도 있고, 어떤 네임스페이스 안에 있거나 없거나 할 수도 있다.
위의 코드블락처럼, using std::swap 을 선언하면, 3->2->1 순으로 탐색을 하여 적절한 swap 함수를 호출한다. 이때, using std::swap 을 반드시 사용해야 하는데, 그 이유는 doSomething 내부의 swap 이 호출될 때std::swap 을 찾아볼 수 있도록 만들어 주어야 하기 때문이다!
다만, swap 함수를 호출할 때 한정자를 잘못 붙이면 안된다.
// swap 을 호출하는 잘못된 방법
// std 의 swap 이외의 것들은 찾아보지 않는다
// 다만 특수화된 std::swap 은 호출할 수도 있다
std::swap(obj1, obj2);
이제 항목 25의 내용을 정리해 보자.
첫째, 표준에서 제공하는 swap 이 충분히 합리적이라면 아무것도 하지 않아도 된다.
둘째, 표준 swap 의 효율이 별로라면, 다음과 같이 하면 된다.
- 두 객체의 값을 효율적으로 맞바꾸는 함수를 swap 이란 이름으로 만들고, public 멤버 함수로 두자(예외를 던지면 안됨).
- 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap 을 만들고, 1번에서 만든 swap 멤버 함수를 이 미멤버 함수가 호출하도록 만든다.
- 새로운 클래스를 만들고 있다면, 그 클래스에 대한 std:;swap 의 특수화 버전을 준비하자. 이 특수화 버전에서도 swap 멤버 함수를 호출하도록 만든다.
셋째이자 마지막으로, 사용자 입장에서 swap 을 호출할 때, swap 을 호출하는 함수가 std::swap 을 볼 수 있도록 using 선언을 반드시 포함시킨다! swap 을 호출할 때는 네임스페이스 한정자를 붙이지 않도록 주의하자.
위에서 멤버 버전의 swap 은 예외를 던지지 않도록 만들라고 했는데, 그 이유는 swap 을 진짜 쓸모 있게 응용하는 방법들 중 클래스(및 클래스 템플릿)가 강력한 예외 안전성 보장(strong exception-safety guarntee) 를 제공하도록 도움을 주는 방법이 있기 때문이다. 항목 29를 참고하도록 하자!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 27 : 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 26 : 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2022.10.25 |
Effective C++ | 항목 24 : 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2022.10.25 |
Effective C++ | 항목 23 : 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2022.10.25 |
Effective C++ | 항목 22 : 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2022.10.25 |