언어/C++

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

지나가던 개발자 2020. 1. 25. 22:52
반응형

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

 

swap는 자기대입 현상의 가능성에 대처하기 위한 대표적인 메커니즘으로서 널리 사랑받아 왔습니다.

두 객체의 값을 '맞바꾸기'한다는 것은 각자의 값을 상대방에게 주는 동작입니다.

표준 swap가 동작시 복사가 세 번 일어납니다. 이런 복사가 일어날시 손해를 보는 타입들 중 으뜸을 꼽는다면 아마도 다른 타입의 실제 데이터를 가리키는 포인터가 주성분인 타입일 것입니다.

 

이러한 개념을 쓰고 있는 기법이 바로 pImpl입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class WidgetImpl {
public :
    ...
 
private :
    int a, b, c;
    std::vector<double> v;
    ...
};
 
class Widget {
public:
    Widget(const Widget& rhs);
 
    Widget& operator=(const Widget& rhs)        // Widget을 복사하기 위해, 자신의 WidgetImpl 객체를 복사합니다.
    {
        ...
        *pImpl = *(rhs.pImpl);
        ...
    }
    ...
 
private :
    WidgetImpl * pImpl;                            // Widget의 실제 데이터를 가진 객체에 대한 포인터
};
Colored by Color Scripter
 

 

이렇게 만들어진 Widget 객체를 우리가 직접 맞바꾼다면, pImpl 포인터만 살짝 바꾸는 것 말고는 실제로 할 일이 없습니다. 그래서 조금 손을 보고 싶습니다. std::swap에다가 뭔가를 알려 주는거죠. Widget 객체를 맞바꿀 떄는 내부의 pImpl 포인터만 맞바꾸라고 말입니다. 기본 아이디어만 코드로 보여드리겠습니다. 아직 컴파일은 되지 않습니다.

 

1
2
3
4
5
6
7
8
namespace std {
 
    template<>
    void swap<Widget>(Widget& a, Widget& b)            // 이 코드는 T가 Widget일 경우에 대해 std::swap을 특수화한 것입니다.
    {
        swap(a.pImpl, b.pImpl);
    }
}
Colored by Color Scripter
 

 

함수 시작 부분에 잇는 'template<>'가 std::swap의 완전 템플릿 특수화 함수라는 것을 컴파일러에게 알려 주는 부분입니다. 일반적으로 std 네임스페이스의 구성요소는 함부로 변경하거나 할 수 없지만, 프로그래머가 직접 만든 타입에 대해 표준 템플릿을 완전 특수화하는 것은 허용이 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
    }
}
Colored by Color Scripter
 

 

이렇게 작성하면 컴파일될 뿐만 아니라, 기존의 STL 컨테이너와 일관성도 유지되는 착한 코드가 되었습니다.

 

함수 템플릿을 부분적으로 특수화하고 싶을수도 있습니다. 하지만 C++는 클래스 템플릿에 대해서는 부분 특수화를 허용하지만 함수 템플릿에 대해서는 허용하지 않도록 정해져 있습니다.

 

함수 템플릿을 '부분적으로 특수화'하고 싶을 때는 오버로드 버전을 하나 추가하면 됩니다.

일반적으로 함수 템플릿의 오버로딩은 해도 별 문제가 없지만, std는 조금 특별한 네임스페이스이기 때문에 이 네임스페이스에 대한 규칙도 다소 특별합니다. std 내의 템플릿에 대한 완전 특수화는 OK이지만, std에 새로운 템플릿을 추가하는 것은 OK가 아닙니다.

 

우리는 swap을 호출해서 우리만의 효율 좋은 '템플릿 전용 버전'을 쓸 수 있으면 좋겠습니다. 이를 위한 방법은 간단합니다. 멤버 swap를 호출하는 비멤버 swap를 선언해 놓되, 이 비멤버 함수를 std::swap의 특수화 버전이나 오버로딩 버전으로 선언하지만 않으면 됩니다.

 

정리를 해보겠습니다.

첫째, 표준에서 제공하는 swap이 여러분의 클래스 및 클래스 템플릿에 대해 납득할 만한 효율을 보이면, 그냥 아무것도 하지 마세요

둘째, 표준 swap의 효율이 기대한 만큼 충분하지 않다면, 다음과 같이 하십시오.

1. 여러분의 타입으로 만들어진 두 객체의 값을 바꾸는 함수를 swap라는 이름으로 만들고, 이것을 public 멤버 함수로 두십시오. 이 함수는 절대로 예외를 던져선 안 됩니다.

2. 클래스 혹은 템플릿이 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap을 만들어 넣습니다. 그리고 1번에서 만든 swap 멤버 함수를 이 비멤버 함수가 호출하도록 만듭니다.

3. 새로운 클래스를 만들고 있다면, 그 클래스에 대한 std::swap의 특수화 버전을 준비해 둡니다. 그리고 이 특수화 버전에서도 swap 멤버 함수를 호출하도록 만듭니다.

셋째, 사용자 입장에서 swap을 호출할 때, swap을 호출하는 함수가 std::swap을 볼 수 있도록 using 선언을 반드시 포함시킵니다. 그 다음에 swap을 호출하되, 네임스페이스 한정자를 붙이지 않도록 하십시오.

 

꼭 잊지 말아야 할 것!

1. std::swap이 여러분의 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공합시다. 이 멤버 swap은 예외를 던지지 않도록 만듭시다.

2. 멤버 swap을 제공했으면, 이 멤버를 호출하는 비멤버 swap도 제공합니다. 클래스(템플릿이 아닌)에 대해서는, std::swap도 특수화해 둡시다.

3. 사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출합시다.

4. 사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능합니다. 그러나 std에 어떤 것이라도 새로 '추가'하려고 들지는 마십시오.

반응형