본문 바로가기

Effective C++/4. 설계 및 선언

항목 25: 예외를 던지지 않는 swap에 대한 지워도 생각해 보자.

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일까?

 

  1. std::swap                                   // 무조건 있음
  2. std::swap 완전특수화 버전          // 있을 수도, 없을 수도 있음
  3. T 타입 전용의 버전                      // 있을 수도, 없을 수도 있고, 어떤 네임스페이스 안에 있거나 없을 있음(하지만 std에는 확실히 없음)

 

우리는 호출 우선순위를 3->2->1 하고 싶다.

우선순위를 어떻게 정함? => 아까 나왔던 컴파일러의 인자기반탐색 규칙을 활용하면

 

 

컴파일러가 적절한 swap 찾을 , 인자기반탐색 규칙으로 인해

  1. 전역네임스페이스와 타입T 동일한 네임스페이스 안에 T 전용 swap 있는지 찾는다.
  2. using std::swap으로 인해 std::swap 찾는다. T 특수화된 std::swap 있으면 우선시한다.
  3. std::swap 일반버전을 호출한다.

 

만약 using std::swap 없고, T 타입 전용 비멤버 swap 없다면 다음과 같은 에러가

 

그래서 만약 T전용 비맴버 swap 없더라도 std::swap 호출할 있게 using std::swap; 써서 std::swap 컴파일러가 있게 해준다.

 

======================================================================

 

정리하면,

 

전제 : std::swap 우리가 만든 클래스(또는 클래스 템플릿) 대해 남득할 만한 효율을 보이면 그냥 아무것도 하지 말자(딱히 특수화나 비맴버함수를 찾을 필요가 없다,)

 

하지만 std::swap 효율적이지 않다고 생각하면(pimpl 관용구와 비슷하게 만들어진 클래스면 십중팔구)

 

  1. 우리가 만든 타입으로 만들어진 객체의 값을 빠르게 바꾸는 함수를 swap이라는 이름으로 만들고, 이를 public으로 둔다.( 맴버함수는 절대 예외를 던져서는 안된다. noexcept 키워드 넣는걸 추천)
  2. 우리의 클래스가 들어있는 네임스페이스와 같은 네임스페이스에 비맴버 swap 만들어 넣고 1번에서만든 멤버함수를 호출하도록 만들자.
  3. 새로운 클래스(클래스 템플릿이 아니고) 만들고 있다면, 클래스에 대한 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 어떤 것이라도 새로 '추가'하려고 들지는 말자.