swap : 예외 안전성 보장(항목 29) + 자기대입 방지(항목 11)
=> 중요한 만큼 구현을 잘하는 것이 중요함
어떻게 구현? => 기본적으로 표준 라이브러리에서 제공하는 swap 알고리즘을 사용한다. 보면 우리가 사용하는 것과 똑같이 구현되있음
한번 호출에 복사가 3번되는 것을 볼 수 있다. 하지만 타입에 따라서는 사본 없이 복사를 할 수 있는 경우도 있는데, 이때는 이 복사 3번이 매우 불필요할 수 있다.
대표적으로 복사하면 손해보는 타입은 다른 타입의 실제 데이터를 가르키는 포인터가 주성분인 타입들
이러한 개념을 설계로 이어나간 기법이 pimpl(pointer to implementation) 관용구(idiom) 이다. 예제를 바로 보자. ([C++] pImpl(pointer to implementation)를 이용해서 인터페이스와 구현을 분리하기 (tistory.com))
Widget 객체를 swap한다면 pimpl만 바꾸면 된다. 하지만 표준라이브러리 swap은 또 Widget 객체 세 개를 복사할 것이고, 그것도 모자라서 WidgetImpl 객체도 세 개 복사할 것이다.(deep copy 지원시)
(Widget의 복사 생성자가 shallow copy 지원할 때)
(Widget의 복사 생성자가 deep copy를 지원할 때
이건 너무 어지러움 -> pimpl만 서로 교체되게 하고 싶다. -> std::swap을 Widget에 대해 특수화 해서 Widget에 대한 swap은 다르게 동작하도록 설정 가능
swap을 friend로 등록하면 해결가능? => 표준 라이브러리와 다른 동작 => 좋지 않다.
=> Widget안에 swap이라는 public 맴버 함수를 선언 후 그 함수가 실제 맞바꾸기를 수행하도록 만든 후, std::swap 특수화 함수에선 그 맴버함수를 호출만 하게 하자.
컴파일도 되고 기존의 STL 컨테이너와의 일관성도 유지됨. 근데 문제가 딱 하나 있는데, Widget과 WidgetImpl이 클래스가 아니라 템플릿 클래스이면??
이건 부분 특수화이다. (완전 특수화면 한 타입에 대해서 재정의하는 것임) => 함수 템플릿은 부분 특수화가 안됨. 애초에 문법 자체가 좀 이상한거 같은데..
함수 템플릿을 부분 특수화 하고 싶다 => 오버로드 버전을 하나 추가하면 됨
일반적으로 함수 템플릿의 오버로딩은 해도 상관없지만, std는 좀 특별한 네임스페이스라서 일반적인 네임스페이스와 규칙이 다르다. 결과만 말하면 std 내의 템플릿에 대해 완전 특수화는 가능하지만, std에 새로운 템플릿을 추가하는 것은 불가능하다.
std namespace에는 어떠한 것도 '추가'는 안됨
위의 표현은 문법적으로는 맞다. 그래서 컴파일도 된다. 하지만 실행되는 결과는 '미정의 사항' 이다. 즉 logical error가 날 수 있다.
그럼 진짜 어캄 => 맴버 swap을 호출하는 비맴버 swap은 선언, 이 맴버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언안하면 된다.(즉 std 네임스페이스에 추가만 안되면 됨)
Widget2Stuff는 Widget2에 관련 기능이 전부 들어 있음
이렇게 가능, 3번째가 가능한 이유는 컴파일러는 C++의 인자 기반 탐색(쾨니그 탐색) 규칙으로 인해 자동으로 Widget2Stuff의 swap을 찾아낸다. (함수 인자가 있으면, 함수 이름을 찾기 위해 해당 타입의 인자가 위치한 네임스페이스의 이름부터 찾아들어감)
우리가 만든 특정 클래스 타입 전용 swap이 되도록 많은 곳에서 호출될려면, 그 클래스와 동일한 네임스페이스 안에 비멤버 버전 swap 구현, std::swap의 특수한 버전 둘다 준비 하는 것이 좋다.
위 코드는 사실 namepspace를 안 써도 잘 동작함. 근데 굳이 전역 네임스페이스를 쓸 이유가 없다.
==================================================================
우리가 어떤 함수 템플릿을 만들고 있는데, 여기서 두 객체를 swap해야 할 일이 생긴다고 해보자. 이때 이 swap은 어떤 swap일까?
- std::swap // 무조건 있음
- std::swap의 완전특수화 버전 // 있을 수도, 없을 수도 있음
- T 타입 전용의 버전 // 있을 수도, 없을 수도 있고, 어떤 네임스페이스 안에 있거나 없을 수 있음(하지만 std에는 확실히 없음)
우리는 호출 우선순위를 3->2->1로 하고 싶다.
이 우선순위를 어떻게 정함? => 아까 나왔던 컴파일러의 인자기반탐색 규칙을 활용하면 됨
컴파일러가 적절한 swap을 찾을 때, 인자기반탐색 규칙으로 인해
- 전역네임스페이스와 타입T와 동일한 네임스페이스 안에 T 전용 swap이 있는지 찾는다.
- using std::swap으로 인해 std::swap을 찾는다. 이 때 T에 특수화된 std::swap이 있으면 우선시한다.
- std::swap의 일반버전을 호출한다.
만약 저 using std::swap이 없고, T 타입 전용 비멤버 swap이 없다면 다음과 같은 에러가 남
그래서 만약 T전용 비맴버 swap이 없더라도 std::swap을 호출할 수 있게 using std::swap; 을 써서 std::swap을 컴파일러가 볼 수 있게 해준다.
======================================================================
정리하면,
전제 : std::swap이 우리가 만든 클래스(또는 클래스 템플릿)에 대해 남득할 만한 효율을 보이면 그냥 아무것도 하지 말자(딱히 특수화나 비맴버함수를 찾을 필요가 없다,)
하지만 std::swap이 효율적이지 않다고 생각하면(pimpl 관용구와 비슷하게 만들어진 클래스면 십중팔구)
- 우리가 만든 타입으로 만들어진 두 객체의 값을 빠르게 바꾸는 함수를 swap이라는 이름으로 만들고, 이를 public으로 둔다.(이 맴버함수는 절대 예외를 던져서는 안된다. noexcept 키워드 넣는걸 추천)
- 우리의 클래스가 들어있는 네임스페이스와 같은 네임스페이스에 비맴버 swap을 만들어 넣고 1번에서만든 멤버함수를 호출하도록 만들자.
- 새로운 클래스(클래스 템플릿이 아니고)를 만들고 있다면, 그 클래스에 대한 std::swap의 특수화 버전을 준비해 두자. 그리고 이 특수화 버전에서도 swap 맴버함수를 호출하도록 하자.
그리고 사용자 입장에서 swap을 호출할 때(doSomething), swap을 호출하는 함수가 std::swap을 볼 수 있도록 using 선언을 반드시 포함시키자. 그 다음에 swap을 호출하되, 네임스페이스 한정자를 붙이지 말자.
여기서 1번에 맴버 swap은 절대 예외를 던지면 안된다. => 왜냐하면 swap을 진짜 효율적으로 사용하는 방법들 중 클래스(또는 클래스 템플릿)가 예외 안전성 보장(strong exception-safty guarantee, 어떤 연산이 실행되다가 예외가 발생하면 그 연산이 시작되기 전의 상태로 돌릴 수 있다는 보장)를 제공하도록 도움을 주는 방법이 있기 때문이다.(항목 29 참고)
맴버 버전에만 해당된다. => 비맴버 버전은 애초에 맴버버전 호출, std::swap은 복사 생성, 복사 대입에 기반하고 있는데 이들은 기본적으로 예외가 혀옹되기 때문이다.
효율과 예외금지는 세트인 경우가 많다. 효율이 대단히 좋은 swap함수는 거의 항상 기본타입제공(pimpl 관용구 기반의 설계에서 쓰이는 포인터처럼)을 사용한 연산으로 만들어지기 때문이다. 그리고 기본제공 타입을 사용한 연산은 절대로 예외를 던지지 않는다.
======================================================================
- std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면, swap 맴버 함수를 제공하자. 이때 이 맴버 swap은 예외를 던지지 않도록 만들자.
- 맴버 swap을 제공했으면, 이 맴버를 호출하는 비맴버 swap도 제공하자. 클래스(템플릿이 아닌 경우)에 대해서는 std::swap도 특수화 해두자.
- 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어준 후에 네임스페이스 한정자 없이 swap을 호출하자.
- 사용자 정의 타입에 대한 std 템플릿을 '완전' 특수화하는 것은 가능, 그러나 std에 어떤 것이라도 새로 '추가'하려고 들지는 말자.
'Effective C++ > 4. 설계 및 선언' 카테고리의 다른 글
항목 24: 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자. (0) | 2021.04.24 |
---|---|
항목 23: 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2021.04.24 |
항목 22: 데이터 맴버가 선언될 곳은 private 영역임을 명심하자. (0) | 2021.04.24 |
항목 21: 함에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자. (0) | 2021.04.24 |
항목 20: '값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다. (0) | 2021.04.24 |