[C++] 항목 25: 예외를 던지지 않는 swap에 대한 지원도 생각해 보자

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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을 탐색한다:

    1. 전역 유효범위 혹은 타입 T와 동일한 네임스페이스 안에 T 전용의 swap이 있는지 찾는다.
    2. 없으면 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 호출

✅ 공통 규칙

  1. swap 멤버 함수는 예외를 던지면 안 됨

    → 강력한 예외 안전성을 위한 기반이 된다. (항목 29에서 더 자세히 나옴)

  2. 사용자 코드에서는 반드시

using std::swap;
swap(obj1, obj2);

형태로 호출해야 ADL이 제대로 작동한다.

  1. std 네임스페이스에 함수 오버로딩은 절대 금지, → 완전 특수화는 가능

  2. 새로운 클래스를 만들 때, 해당 클래스에 대한 std::swap의 특수화 버전을 준비하는 것도 좋은 선택이다. → 단, 클래스 템플릿인 경우엔 네임스페이스 내부 비멤버 swap으로 처리

Cpp 카테고리 내 다른 글 보러가기

댓글남기기