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

수민이슈·2023년 3월 28일
0

Effective C++

목록 보기
25/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자.
(이 멤버 swap은 예외를 던지지 않도록 하자)
💡 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공하자.
템플릿이 아닌 클래스에 대해서는, std::swap도 특수화해두자
💡 사용자 정의 타입에 대한 std 템플릿을 완전히 특수화하는 것은 가능하다
but std에 어떤 것이라도 새로 추가할 수는 없다


🖊️ swap 함수

swap은 예외 안전성 프로그래밍의 끝판왕 감초이다.
자기 대입 현상의 가능성에 대처하기 위한 대표적인 매커니즘이다.

std namespace에 구현되어 있는 swap은 각자의 값을 상대방에게 주는 동작이다.

namespace std {
	template<typename T>
    void swap(T& a, T& b) {
    	T temp(a);
        a = b;
        b = temp;
    }
}

이렇게 구현되어 있다.

복사(복사 생성자, 복사 대입 연산자)만 제대로 구현되어 있는 타입이면 어떤 객체든 다 바꿔줄 수 있다.

하.지.만

swap 호출 한 번에 복사가 세 번 이루어진다.
너무.. 헤비한 경우가 있을거다

이럴 때 pimpl 설계.

swap이 헤비한 상황

다른 타입의 실제 데이터를 가리키는 포인터가 주 성분인 상황이 주로 swap이 헤비한 상황일거다

class WidgetImpl {
public:
	...
private:
	int a, b, c;
    std::vector<double> v;
    ...
};

class Widget P {
public:
	Widget(const Widget& rhs);
    Widget& operator= (const Widget& rhs) {
    	...
        *pImpl = *(rhs.pImpl);
        ...
    }
    ...
private:
	WidgetImpl* pImpl;
}

이런 상황에서
Widget 타입의 객체을 복사하면
WidgetImpl 타입의 객체를 가리키는 포인터를 복사해줘야 할 것이다.
WidgetImpl 객체는 많은 데이터가 있고,, 너무 무거워서
복사를 해버리면 너무 속상하다.

std::swap을 사용하면
Widget을 세 번 복사하고
WidgetImpl도 세 번 복사할거기 때문이다
...

그래서,, 이 경우에는 pImpl 포인터만 바꾸라고 알려주고 싶다.
자,,
swap을 구현해보자


🖊️ swap 직접 구현해보기

std::swap을 Widget에 대해 특수화 (specialize)

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);
    }
}

template<>는 이 함수가 std::swap의 완전 템플릿 특수화 함수라는 것을 의미한다.
swap<Widget>은 T가 Widget인 경우에 대한 특수화라는걸 의미한다

그니까
타입과 무관한 swap 템플릿이었던 친구가
Widget에 적용될 때는
저 swap을 사용해야 한다는 뜻임

그리고

swap 멤버 함수

를 구현했냐면
pImpl 포인터는 private으로 선언되어 있기 때문에
클래스 외부에서 접근할 수가 없음
그래서 클래스에서 public으로 멤버함수를 선언해줬다
그래서
swap의 특수화 함수가 이 멤버함수를 호출하면 된거다.

클래스 템플릿인 경우

그니까
Widget과 WidgetImpl이 클래스가 아니라 클래스 템플릿이라면..
WidgetImpl에 저장된 데이터의 타입을 매개변수로 바꿀 수 있다면,,,?

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);
    }
}

이렇게 할건 아니잖아 ㅠㅠ

C++은 클래스 템플릿에 대해서는 부분 특수화를 허용하지만,
함수 템플릿에 대해서는 허용하지 않아용.

그래서
함수 템플릿을 부분 특수화 하고 싶다면?

그냥
오버로드 버전을 추가하시면 됩니당.

namespace std {
	template<typename T>
    void swap(Widget<T>& a, Widget<T>&b) {
    	a.swap(b);
    }
}

std 내의 템플릿에 대한 완전 특수화는 괜찮지만,
std에 새로운 템플릿을 추가하는건 안됩니당.

std에 뭘 추가하면
미정의 동작을 해버린다.

그니까

비멤버 swap

멤버 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을 호출하더라도
컴파일러는 이름 탐색 규칙에 따라서
WidgetStuff 네임스페이스 내부에 위치한 특수화 버전을 찾아낼거다

네임스페이스에 비멤버 swap을 넣고 std::swap의 특수화도 준비하자


🖊️ swap 사용...

template <typename T>
void doSomething(T& obj1, T& obj2) {
	...
    swap(obj1, obj2);
}

이 상황에서 호출될 swap은 뭘까?

  • std::swap 일반형
  • std::swap 특수화 버전
  • T 타입 전용 swap

흠..
내가 하고 싶은게
T 타입 전용 swap -> std::swap 일반형
이 순서로 호출되게 하고 싶다 (T 타입 전용이 없으면 일반형으로)

그러면

template<typename T>
void doSomething(T& obj1, T& obj2) {
	using std::swap;
	...
    swap(obj1, obj2);
}

이렇게
using std::swap;을 적어주면
std::swap을 이 함수 안으로 끌어들일 수 있당

이 using 선언은 std::swap을 볼 수 있게 해주기 때문에
std의 swap을 쓸 수 있다.

이렇게 선언되어 있더라도
std::swap의 T 전용 버전을 일반형 템플릿보다 더 먼저 선택하게 되어 있으니까
T에 대한 std::swap의 특수화 버전이 먼저 쓰이게 된다 (있으면)

하 지 만
호출문에 한정자를 붙이지 말자

std::swap(obj1, obj2);이렇게 하면

무조건 std::swap의 일반형을 호출하게 된다.

but
클래스에 대해 std::swap을 완전히 특수화해놓으면
이렇게 잘못 한정화된 호출문으로도 T 타입 전용 swap 함수를 끌어다 쓸 수 있긴 하다,,
어렵다


🖊️ 정리

일단,
웬만하면 std의 swap을 그냥 써라.
근데 사용자 정의 클래스 및 클래스 템플릿에 대해 효율이 너무 떨어지면,,

public으로 swap 멤버 함수를 만들자 (예외 던지기 X)

멤버 버전의 swap이 예외를 던지면 안되는 이유는
클래스가 강력한 예외 안전성 보장을 제공하도록 도와주는 방법이 있는데
그게 멤버 버전의 swap이 예외를 던지면 안된다는 가정하에 있다.
이거는 항목 29에서 후술할거당

클래스, 템플릿과 동일한 namespace에 멤버 swap을 호출하는 비멤버 swap을 만들자

새로운 클래스(클래스 템플릿 X)에 대해 swap 멤버함수를 호출하는 std::swap의 특수화 버전을 준비하자

사용자 입장에서 swap을 호출할 때, using 선언을 반드시 포함하자.


😊

역대급,, 어려웠따
이거는 두고두고 봐야겠다

0개의 댓글