스토리텔링 개발자

[Effective C++] 25. 예외를 던지지 않는 swap 함수 본문

개발/Effective C++

[Effective C++] 25. 예외를 던지지 않는 swap 함수

김디트 2024. 6. 19. 11:30
728x90

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

 

 

 

swap 함수
  • 다양한 활용성
    • 예외 안전성 프로그래밍(항목 29 참조)의 감초 역할로 쓰인다.
    • 자기 대입 현상(항목 11 참조)을 대처하기 위한 대표적인 매커니즘으로 쓰인다.
  • 쓸모가 많기에 구현 방법이 중요하다.
  • 그렇다면 어떻게 만들어야 쓸만한 swap을 만들 수 있을까?

 

 

STL이 제공하는 swap 함수의 구현
namespade std
{
    template<typename T>
    void swap(T& a, T& b)
    {
        T temp(a);
        a = b;
        b = temp;
    }
}
  • T 객체가 복사만 지원해준다면 정상 동작한다.
  • 호출 1번, 복사 3번이 발생한다.

 

 

 

복사하면 손해를 보는 타입
  • (다른 타입의 실제 데이터를 가리키는)포인터가 주성분인 경우
    • ex) pimpl 관용구
      • class WidgetImpl // 여러 데이터를 포함. 즉, 복사 비용이 높다.
        {
        public:
            ...
        private:
            int a, b, c;
            std::vector<double> v;
            ...
        };
        
        class Widget // pImpl 관용구를 사용한 클래스
        {
        public:
            Widget(const Widget& rhs);
            
            Widget& operator=(const Widget& rhs)
            {
                ...
                *pImpl = *(rhs.pImpl); // WidgetImpl 객체를 복사
                ...
            }
            ...
        private:
            WidgetImpl *pImpl;
        };
    • 포인터만 바꾸면 되므로 복사시 자원 소모가 적다.
    • 이 경우 swap 함수에서 실제로 하면 되는 일은?
      • 존재하는 두 개의 Widget 객체 내부 pImpl만 교체하면 된다.
      • 즉,  WidgetImpl 포인터 세 개를 복사하여 swap하면 끝난다.
    • 하지만 표준 swap 알고리즘의 구현은 아래와 같다.
      • Widget 객체 세 개를 복사하여 swap한다.
      • 즉 WidgetImpl 포인터 세 개를 포함하는 Widget 객체 세 개를 복사하므로 불필요한 자원 소모가 있다.

 

 

 

swap 함수 보완법
  1. 템플릿 특수화(specialize)를 사용한다. 
    • namespace std
      { 
          // Widget 객체에 대한 템플릿 특수화 버전
          template<>
          void swap<Widget>(Widget& a, Widget& b)
          {
              swap(a.pImpl, b.pImpl); // pImpl만 맞바꾼다.
          } 
       }
    • 문제점
      • pImpl에 대한 접근 권한이 없는 경우가 많을 것이다.
      • 이 경우 pImpl은 private 로 감춰져 있기에 컴파일이 되지 않는다.
      • 프랜드 함수로 만들어 해결한다?
        • 표준 템플릿들에 쓰인 규칙과 어긋나므로 좋은 모양은 아니다.
  2. 템플릿 특수화를 사용하되, 클래스가 swap 함수를 public으로 지원한다.
    • class Widget
      {
      public:
          ...
          void swap(Widget& other) // swap 함수 제공
          {
              using std::swap;
              swap(pImpl, other.pImpl);
          } 
          ...
      };
      
      namespace std
      {
          // 템플릿 특수화 버전
          template<>
          void swap<Widget>(Widget& a, Widget& b)
          {
              a.swap(b); // 클래스에서 제공하는 swap 함수를 사용
          }
      }
    • 문제점
      • 아래처럼 객체가 클래스가 아니라 클래스 템플릿으로 만들어진 경우 위 코드는 컴파일이 되지 않는다..
        • template<typename T>
          class WidgetImpl { ... };
          
          templcate<typename T>
          class Widget { ... };
        • C++에서는 클래스 템플릿의 부분 특수화는 허용이 되지만 함수 템플릿의 부분 특수화는 허용이 되지 않는다.
          • 완전 특수화 : 하나의 클래스에 대한 특수화
          • 부분 특수화 : 다수의 클래스에 대한(즉, 또 다른 템플릿 클래스에 대한) 특수화
        • namespace std
          {
              template<typename T>
              void swap< Widget<T> >(Widget<T>& a, Widget<T>& b) // 이런 구성은 부분 특수화이므로 에러
              {
                  a.swap(b);
              }
          }
  3. 오버로드 버전을 하나 추가한다.
    • namespace std
      {
          // std::swap을 오버로드한 함수
          template<typename T>
          void swap(Widget<T>& a, Widget<T>& b)
          {
              a.swap(b);
          }
      }
    • 문제점
      • std는 특별한 namespace이므로, 이 네임스페이스에 대한 규칙도 다소 특별하다.
      • std 내의 템플릿에 대한 완전 특수화는 괜찮지만, std 내에 새로운 템플릿을 추가하는 것은 안된다.
        • 클래스, 함수 어떤 것도 안된다.
      • 컴파일은 될지라도 결과는 미정의 사항!!
      • 그러므로 이 방식은 사용하면 안 된다.
  4. 멤버 swap을 호출하는 비멤버 swap을 선언하되, 커스텀 네임스페이스로 그 모든 걸 감싼다.
    • namespace WidgetStuff
      {
          template<typename T>
          class Widget { ... };
          
          
          // 비멤버 swap 함수 선언
          template<typename T>
          void swap(Widget<T>& a, Widget<T>& b)
          {
              a.swap(b);
          }
      }
    • 해당 비멤버 swap은 C++의 이름 탐색 규칙(인자 기반 탐색 혹은 쾨니그 탐색)에 의해 std 버전보다 먼저 발견된다.
    • 즉, 그 클래스와 동일한 네임스페이스 안에 비멤버 swap으로 선언하면 해결된다.
    • 네임스페이스를 사용하지 않아도 위 사항은 유효하지만...
      • 전역 네임스페이스에 온갖 이름을 때려넣는 건 관리 측면에서 안 좋으므로 가능하면 네임스페이스로 감싸자.

 

 

 

인자 기반 탐색(argument-dependent lookup)(ADL)
  • 어떤 함수에 어떤 타입의 인자가 있으면, 그 함수의 이름을 찾기 위해 해당 타입의 인자가 위치한 네임스페이스 내부의 이름을 탐색해 들어간다는 규칙

 

 

 

함수 템플릿 내부에서 swap을 사용할 때 탐색 순서
template<typename T>
void doSomething(T& obj1, T& obj2)
{
    using std::swap; // std::swap을 이 함수 안으로 끌어올 수 있도록 만든다.
    ...
    swap(obj1, obj2); // 여기서 어떤 swap을 호출해야 할까?
    
    // std::swap(obj1, obj2);
    // 이 사용법은 다른 swap 함수의 가능성을 배제하므로 좋지 않다.

    ...
}
  1. 전역 유효범위 혹은 타입 T와 동일한 네임스페이스 안에 T 타입 전용의 swap 버전 탐색 (swap 함수 보완 4번 케이스)
  2. std의 일반형을 특수화한 버전 탐색 (swap 함수 보완 2번 케이스)
    • 앞의 using 문으로 인해 std::swap을 볼 수 있으므로 탐색하게 된다.
  3. std에 있는 일반형 버전 (보편적인 케이스)
    • 확실히 존재한다.

 

 

 

정리
  • 표준 제공 swap이 효율이 괜찮으면 아무것도 하지 말자.
  • 기대만큼의 효율이 없다면
    1. public swap 멤버 함수를 만든다.
      • 이 경우 절대 예외를 던지면 안된다.
      • 클래스(및 클래스 템플릿)가 강력한 예외 안전성 보장을 제공하도록 도움을 주는 방법이 있기 때문이다.(항목 29 참조)
    2. 해당 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap을 만들어 넣는다.
      • 1번 멤버 함수를 내부적으로 호출하게 한다.
    3. 새로운 클래스(클래스 템플릿이 아닌 경우)를 만들고 있다면, 그 클래스에 대한 std::swap의 특수화 버전을 준비해 둔다.
      • 1번 멤버 함수를 내부적으로 호출하게 한다.

 

728x90
Comments