KoreanFoodie's Study

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

Tutorials/C++ : Advanced

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

GoldGiver 2022. 10. 25. 16:15

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

1. std::swap 이 사용자 정의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자. 이 멤버 swap 은 예외를 던지지 않도록 만들자.
2. 멤버 swap 을 제공했으면, 이 멤버를 호출하는 비멤버 swap 도 제공하자. 클래스(템플릿이 아닌)에 대해서는, std::swap 도 특수화해 두자.
3. 사용자 입장에서 swap 을 호출할 때는, std::swap 에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap 을 호출하자.
4. 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하다. 그러나 std 에 어떤 것이라도 새로 '추가' 하려고 들지는 말자.

 

먼저 표준 라이브러리에서 제공하는 기본 swap 알고리즘 구현을 보자.

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

위의 코드는 복사 생성자 및 복대 대입 연산자를 호출한다.

손해를 줄이기 위해, 포인터 복사를 응용한 pimpl 기법을 보자(pointer to implementation).

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

class Widget
{
  public:
    Widget(const Widget& rhs);
    // Widget 복사를 위해 포인터를 복사
    Widget& operator=(const Widget& rhs)
    {
      *pImpl = *(rhs.pImpl);
    }
  private:
    // Widget 의 실제 데이터를 가진 객체에 대한 포인터
    WidgetImpl *pImpl;
};

std::swap 을 특수화하면, 위의 WidgetImpl 을 이용한 효율적 swap 을 구현할 수 있다.

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

하지만 위 코드는 컴파일이 안된다. 왜냐하면 pImpl 이 private 이기 때문이다. Widget 클래스를 다음과 같이 조금 고쳐보자.

class Widget
{
  public:
    void swap(Widget& other)
    {
      // 해당 선언문은 왜 필요할까? (조금 이따가 공개...)
      using std::swap;
      swap(pImpl, ohter.pImpl);
    }
  
  ...  
    
  private:
    WidgetImpl *pImpl;
};

// swap 특수화 버전도 살짝 수정한다
namespace std
{
  template<>
  void swap<Widget>(Widget& a, Widget& b)
  {
    a.swap(b);
  }
}

이제 swap 이 정상적으로 작동한다!

 

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

template<typename T>
class WidgetImpl { ... };

template<typename T>
class Widget { ... };

namespace std
{
  // 에러! 적법하지 않은 코드!
  template<typename T>
  void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
  {
    a.swap(b);
  }
}

C++ 는 클래스 템플릿에 대해서는 부분 특수화(partial specialization) 을 허용하지만 함수 템플릿에 대해서는 허용하지 않도록 정해져 있다. 따라서 컴파일이 되지 않는다!

함수 템플릿을 '부분적으로 특수화' 하고 싶을 때 흔히 취하는 방식은 다음과 같이 오버로드 버전을 하나 추가하는 것이다.

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

일반적으로 함수 템플릿의 오버로딩은 별 문제가 없다. 하지만 std 는 조금 특별해서, std 내의 템플릿에 대한 완전 특수화는 OK 지만, 새로운 템플릿을 추가하는 것은 금지되어 있다. 컴파일은 되지만 미정의 동작이 발생하므로, 절대 추가하지 말자!

결론은, 멤버 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 을 호출하더라도, 컴파일러는 C++ 의 탐색 규칙(인자 기반 탐색 : argument-dependent lookup) 혹은 쾨니그 탐색(Koenig lookup) 에 의해 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아낸다.

 

그렇다면 다음 코드에서는 어떤 swap 이 호출될까?

template<typename T>
void doSomething(T& obj1, T& obj2)
{
  // std::swap 을 이 함수 안으로 끌어올 수 있도록 만드는 문장
  using std::swap;
  ...
  // T 타입 전용의 swap 을 호출한다
  swap(obj1, obj2);
}

 3 가지 가능성이 있다.

  1. std 에 있는 일반형 버전 : 이건 확실히 있다
  2. std 의 일반형을 특수화한 버전 : 이건 있을 수도, 없을 수도 있다
  3. T 타입 전용의 버전 : 있을 수도, 없을 수도 있고, 어떤 네임스페이스 안에 있거나 없거나 할 수도 있다.

위의 코드블락처럼, using std::swap 을 선언하면, 3->2->1 순으로 탐색을 하여 적절한 swap 함수를 호출한다. 이때, using std::swap 을 반드시 사용해야 하는데,  그 이유는 doSomething 내부의 swap 이 호출될 때std::swap 을 찾아볼 수 있도록 만들어 주어야 하기 때문이다!

다만, swap 함수를 호출할 때 한정자를 잘못 붙이면 안된다.

// swap 을 호출하는 잘못된 방법
// std 의 swap 이외의 것들은 찾아보지 않는다
// 다만 특수화된 std::swap 은 호출할 수도 있다
std::swap(obj1, obj2);

 

이제 항목 25의 내용을 정리해 보자.

첫째, 표준에서 제공하는 swap 이 충분히 합리적이라면 아무것도 하지 않아도 된다.

둘째, 표준 swap 의 효율이 별로라면, 다음과 같이 하면 된다.

  1. 두 객체의 값을 효율적으로 맞바꾸는 함수를 swap 이란 이름으로 만들고, public 멤버 함수로 두자(예외를 던지면 안됨).
  2. 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap 을 만들고, 1번에서 만든 swap 멤버 함수를 이 미멤버 함수가 호출하도록 만든다.
  3. 새로운 클래스를 만들고 있다면, 그 클래스에 대한 std:;swap 의 특수화 버전을 준비하자. 이 특수화 버전에서도 swap 멤버 함수를 호출하도록 만든다.

셋째이자 마지막으로, 사용자 입장에서 swap 을 호출할 때, swap 을 호출하는 함수가 std::swap 을 볼 수 있도록 using 선언을 반드시 포함시킨다! swap 을 호출할 때는 네임스페이스 한정자를 붙이지 않도록 주의하자.

 

위에서 멤버 버전의 swap 은 예외를 던지지 않도록 만들라고 했는데, 그 이유는 swap 을 진짜 쓸모 있게 응용하는 방법들 중 클래스(및 클래스 템플릿)가 강력한 예외 안전성 보장(strong exception-safety guarntee) 를 제공하도록 도움을 주는 방법이 있기 때문이다. 항목 29를 참고하도록 하자!

Comments