[Effective C++] #25_예외를 던지지 않는 swap
Scott Meyers의 "Effective C++" 를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 항목은 4장 '설계 및 선언', 항목 25 "예외를 던지지 않는 swap에 대한 지원"에 해당하는 내용입니다.
Concept
namespace std {
template<typename T>
void swap (T& a, T& b){
T temp(a);
a = b;
b = temp;
}
}
CS 전공자라면, 한번쯤 해봤을 "swap"에 대한 구현입니다. 25번 항목은 C++의 STL에 포함된 swap의 효율적인 구현을 목표로 합니다. 먼저, swap은 말 그대로 "값의 맞바꾸기" 입니다. 코드를 통해 swap 알고리즘을 살펴보겠습니다. 복사 생성자 및 복사 대입 연산자를 지원하는 어떤 타입의 객체든 이 swap을 통해 값을 맞바꾸는것이 가능합니다. 위 코드를 살펴보면 T& 형식의 a 객체와 b 객체를 인수로 받아, 제 3의 일시적으로 필요한 객체 temp를 선언하고, 값 맞바꾸기를 진행합니다. 기본적인 내용이므로 자세한 구현 내용은 생략합니다. 이때, 책은 이 swap 알고리즘의 비효율성을 지적합니다. 바로, 한번 호출에 3번 복사가 일어난다는 얘기죠(a -> temp, b -> a, temp -> b).
1. 완전 템플릿 특수화 (Total Template Specialization)
// 인터페이스 구현 클래스
class AImpl {
public:
...
private:
int a, b, c;
std::vector<double> v;
};
// 인터페이스 제공 클래스
class A {
public:
A(const A& rhs); // rhs = "right hand side"
A& operator=(const A& rhs)
{
...
*pImpl = *(rhs.pImpl); // pImpl = 구현 클래스(AImpl)을 가리키는 포인터
...
}
...
private:
AImpl *pImpl;
}
그래서 저자는 std::swap을 직접 손보고자 합니다. 먼저 살표봐야 할 내용이 있습니다. "pimpl 관용"기법입니다. 자세하게 다루진 않겠지만, 간단한게 설명하자면, 컴파일 의존도를 낮추기 위해 1개의 클래스를 인터페이스를 제공만 하는 클래스와 인터페이스를 직접 구현하는 클래스 로 나누어질때, 제공하는 쪽은 구현 클래스에대한 포인터만 데이터 멤버로 갖는 설계기법을 "pimpl 관용구"라고 설명합니다. 이러한 설계기법을 기억하고 위 예제 코드를 살펴봅시다. A 객체의 복사가 일어날때, 간단히 pImpl 포인터만 교환하여, 보다 효율적인 swap 알고리즘 구현이 가능해 보입니다. 하지만, 기존의 std::swap은 이러한 의도를 알턱이 없죠. 그러므로, 이 책은 std::swap을 클래스 A에 대해 특수화를 진행합니다. A 객체 맞바꾸기를 위한 std::swap을 커스터마이징 하자는 거죠.
1.1 템플릿 완전 특수화 및 멤버 함수
namespace std {
...
template<> // tmeplate<>는 템플릿 특수화 함수라는것을 컴파일러에게 알려줍니다.
void swap<A>(A& a, A& b){
a.swap(b); // pImpl의 swap 발생
}
}
class A {
public:
...
void swap(A& b){ // pImpl은 private 멤버, 멤버함수에서 std::swap 호출
using std::swap;
swap(pImpl, b.pImpl);
}
private:
AImpl *pImpl;
};
먼저 "template<>"은 compiler에게 이 함수는 "템플릿 완전 특수화" 라고 알려줍니다. 그리고 뒤에 나오는 swap<A>는 T가 A가 객체일 경우에 대한 특수화라는 사실을 다시 한번 강조하죠. 우리가 이미 알고 있듯이, std namespace의 구성요소를 함부로 변경하는것은 불가능하지만, 개발자가 직접 작성한 객체 혹은 타입에 대한 템플릿을 완전 특수화 하는것은 가능하다고 합니다!
1.2 템플릿 버전
namespace AStuff {
...
template<typename T>
class A {...}; // swap 멤버함수가 포함되어 있다.
template<typename T>
void swap(A<T> &a, A<T>& b){ // 비멤버 swap 함수, std::swap이 아니다!!
a.swap(b);
}
}
A 클래스 멤버 함수 swap을 호출하는 비멤버 swap을 선언해 놓지만, 이 비멤버 함수가 std::swap의 특수화 버전 혹은 오버로딩 하지 않아야 합니다. 왜냐하면, std 내부의 템플릿에 대한 완전 특수화는 허용되지만, std에 아예 새로운 템플릿을 추가하는것은 허용되지 않습니다(미정의 동작). 따라서, 우리가 정의할 비멤버 함수는 std::swap을 오버로딩 하지 않는, 새로운 비멤버 함수를 AStuff 네임스페이스 안에 정의하는것입니다. 결과적으로, 사용자가 어떠한 두개의 A 클래스 객체에 대한 swap을 실행하게되면, C++의 인자 기반 탐색에 의해, ASuff 네임스페이스 안에 있는 비멤버 swap 함수가 실행됩니다.
2. 사용자 입장, 인자 기반 탐색 이용
template<typename T>
void doSomething(&T obj1, T& obj2){
using std::swap;
...
swap(a, b);
...
}
사용자 입장에서 살펴보겠습니다. 어떤 템플릿 함수를 정의 중, obj1과 obj2 에대한 swap 연산을 진행하려 합니다. 이때, 사용자에게 가능한 경우의수는 3가지입니다. 1. std::swap 함수, 2. std::swap의 완전 특수화 버전, 그리고 3. T 타입 전용 swap 버전. 이때, 우리는 우선순위를 T타입 전용 버전의 swap(비멤버 함수), 그리고 T타입 전용 버전이 없다면, std::swap을 호출하는 방식으로 설계하고자 합니다. 위 코드와 같이 함수 내부에 using std::swap을 통해, 이러한 우선순위를 C++ 컴파일러에게 알려줄 수 있습니다. C++의 인자 기반 탐색에 의해 자연스럽게 먼저 T타입 전용의 swap이 존재하는지 찾아보고, 없다면 std::swap을 이용하기 때문이죠.
Summary, 정리
- 2개의 객체에 대한 swap을 진행하기 전에, 해당 객체의 내부에 swap을 멤버 함수로 정의합니다.
- 객체 혹은 템플릿이 들어있는 동일한 namespace안에 비멤버 swap 함수를 정의 하고, 1번에서 만든 멤버함수를 호출하도록 합니다.
- 새로운 클래스를 생성하고자 하면, 해당 클래스에 대한 특수화 버전의 std::swap을 준비합니다.
- 사용자 입장에서 using std::swap을 사전에 정의합니다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #30_인라인 함수 (0) | 2021.12.30 |
---|---|
[Effective C++] #29_예외 안전성 확보 (0) | 2021.12.28 |
[Effective C++] #28_내부에서 사용하는 객체에 대한 핸들 반환 (0) | 2021.12.28 |
[Effective C++] #27_캐스팅 (0) | 2021.12.27 |
[Effective C++] #26_변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2021.12.27 |