[C++] 항목 25: 예외를 던지지 않는 swap에 대한 지원도 생각해 보자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 4. 설계 및 선언
👉🏻 항목 25: 예외를 던지지 않는 swap에 대한 지원도 생각해 보자
swap은 STL에 포함되어 있으며, 자기 대입 현상의 가능성에 대처하기 위해 대표적으로 사용되고 있다.
이번 항목에선 swap에 대해 알아보는 것이 주제이다.
📌 std::swap
namespace std {
template<typename T>
void swap(T& a, T& b) {
T temp(a);
a = b;
b = temp;
}
}
- 표준에서 제공하는
std::swap
함수 - 복사 생성자 + 복사 대입 연산자만 있으면 어떤 타입이든 동작
- 단, 복사가 총 3번 발생 → 비효율적일 수 있다
이런 상황에서 손해를 보는 타입이 있다.
✅ pimpl 관용구
다른 타입의 실제 데이터를 가리키는 포인터가 주성분인 타입에서 손해를 본다.
이 개념을 pimpl(pointer to implementation)이라고 한다.
class WidgetImpl {
public:
...
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 포인터만 바꿔주면 된다.
하지만 std::swap을 사용한다면:
- Widget 객체 3개 복사
- WidgetImpl 객체 3개 복사
→ 매우 비효율적이다.
✅ 해결 방법
🔹 클래스 템플릿이 아닌 경우
✔ std::swap을 Widget에 대해 특수화
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
swap(a.pImpl, b.pImpl);
}
}
template<>
를 통해 std::swap의 완전 템플릿 특수화(total template specialization)를 해주고 있다.
이제 이 함수는 T가 Widget일 경우에 특수화되어, 해당 코드가 호출된다.
하지만 이 코드는 컴파일되지 않는다.
→ pImpl
포인터가 private 멤버이기 때문이다.
✔ Widget 클래스에 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);
}
}
Widget의 public에 swap 멤버 함수를 호출하는 형태로 짜면 된다.
→ ✅ 안전하고 표준 위반이 아니다.
🔹 클래스 템플릿인 경우
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
❌ 잘못된 접근: std::swap에 대해 부분 특수화
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
- 이 코드는 작동하지 않는다.
- std::swap은 함수 템플릿, Widget은 클래스 템플릿으로 만들어져 있다.
- C++은 클래스 템플릿에 대해선 부분 특수화를 허용한다.
- C++은 함수 템플릿에 대한 부분 특수화를 허용하지 않는다.
→ 컴파일 에러 발생
이 또한 템플릿의 특수화와 관련된 내용이므로, 헷갈린다면 여기를 참고하자.
❌ 또 다른 잘못된 접근: std 네임스페이스에서 오버로딩
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
- ✅ 컴파일은 된다.
- ❌ 하지만, 실행 결과는 미정의 사항(UB)이다.
- std내의 템플릿에 대한 완전 특수화는 가능하다.
- std에 새로운 템플릿을 추가하는 것을 불가능하다.
→ std 네임스페이스에 새로운 템플릿 함수를 추가하는 것은 표준 위반이다!
✔ 올바른 해결 방법: 사용자 네임스페이스에서 오버로딩
namespace WidgetStuff {
...
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget {
public:
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pImpl);
}
...
private:
WidgetImpl<T>* pImpl;
};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
- Widget과 WidgetImpl이 들어 있는 네임스페이스인
WidgetStuff
안에서 비멤버 swap을 만든다. - 그러면 인자 기반 탐색(ADL)에 의해
swap(a, b)
호출 시 이 함수가 선택된다.
✅ 사용자 입장에서 swap 호출법
template<typename T>
void doSomething(T& obj1, T& obj2) {
using std::swap;
...
swap(obj1, obj2);
...
}
-
using std::swap
을 추가한다. -
그러면 컴파일러는 다음 순서로 swap을 탐색한다:
- 전역 유효범위 혹은 타입 T와 동일한 네임스페이스 안에 T 전용의 swap이 있는지 찾는다.
- 없으면 std 네임스페이스에서,
- T에 대한 std::swap 특수화 버전을 찾는다.
- 그것도 없으면 std::swap 일반 버전을 사용한다.
⚠️ 주의할 점
std::swap(obj1, obj2);
-
이렇게 호출하면 네임스페이스가 강제되어 std::swap만 호출된다.
→ 사용자 정의 swap을 사용할 수 없다.
반드시 다음처럼 호출해야 한다:
using std::swap;
swap(obj1, obj2);
🧐 정리
✅ 클래스 템플릿이 아닌 경우
- Widget 클래스에 public 멤버 swap 추가
- std::swap에 대해 완전 특수화(template<>) 가능
- std 특수화 버전에서 멤버 swap 호출
✅ 클래스 템플릿인 경우
- std 네임스페이스에 swap 오버로딩 ❌ → 표준 위반, UB
- 같은 네임스페이스 안에 비멤버 swap을 오버로딩으로 제공
- 이 비멤버 swap은 내부적으로 멤버 swap 호출
✅ 공통 규칙
-
swap 멤버 함수는 예외를 던지면 안 됨
→ 강력한 예외 안전성을 위한 기반이 된다. (항목 29에서 더 자세히 나옴)
-
사용자 코드에서는 반드시
using std::swap;
swap(obj1, obj2);
형태로 호출해야 ADL이 제대로 작동한다.
-
std 네임스페이스에 함수 오버로딩은 절대 금지, → 완전 특수화는 가능
-
새로운 클래스를 만들 때, 해당 클래스에 대한 std::swap의 특수화 버전을 준비하는 것도 좋은 선택이다. → 단, 클래스 템플릿인 경우엔 네임스페이스 내부 비멤버 swap으로 처리
댓글남기기