STL에서도 swap은 구현되어있고, 만약 나보고 swap을 구현하라고 하면 십중팔구 임시 객체를 생성하여 구현할 것이다.
template<typename T>
void Swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
물론 기본제공 타입같은 경우엔 xor-swap이란 꼼수도 존재하긴 한다.
a = a ^ b;
b = a ^ b;
a = a ^ b;
표준 라이브러리에서 제공하는 swap은 구현 코드를 보면 복사만 제대로 지원하는 타입이기만 하면 어떤 객체든 swap이 가능하다. 하지만 뭔가 불합리함이 느껴지긴하다. swap 한 번하는데 복사가 3번이나 일어나고 있다.
복사하면 손해를 보는 타입들 중 대표를 뽑는다면 다른 타입의 실제 데이터를 가리키는 포인터다. 쉽게 말해서 다른 객체의 데이터의 포인터를 복사할 때 손해를 본다.
이런 개념을 설계의 미학으로 쓰고 있는 기법으로 pimpl 관용구(31장 참고)(*주 1)라고 한다.
pimpl 개념을 사용하여 클래스를 만든다면
class WidgetImpl //pimple식 설계
{
private:
int a, b, c;
vector<double> b;
};
class Widget
{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
*pImpl = *(rhs.pImpl);
return (*this);
}
private:
WidgetImpl* pImpl;
};
복사 대입 연산자에서 pImpl의 포인터만 바꾸는것 말고는 하는 일이 없다.
하지만 표준 라이브러리는 이런 방식을 사용하지 않고, 세 번의 복사가 일어나니 구현은 쉽겠지만, 비효율성을 느낀다.
완전 템플릿 특수화(total template specialization)를 사용하면 std::swap에게 pImpl의 포인터만 바꾸면 더 효율적이야 라고 알려줄 방법이 있다.
class Widget
{
public:
...
void swap(Widget& other) //swap public 멤버 함수
{
using std::swap; //this->랑 비슷하게 생각하면 될듯 std::swap사용할꺼라는 선언
swap(pImpl, other.pImpl);
}
private:
WidgetImpl* pImpl;
};
namespace std
{
template<> //템플릿 특수화
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
Widget 클래스에 한해서만 std::swap이 Widget 클래스의 멤버 함수인 swap을 사용하라고 특수화한 것이다.
사실상 멤버 함수인 swap에서 std::swap을 사용하여 복사 세 번이 일어난건 사실지만, Widget 클래스 통체로 복사 세 번을 pImple만 복사 세 번으로 바꾼거다. 이게 pimpl 관용구의 힘이다.
만약 Widget과 WidgetImple이 클래스 템플릿으로 만들어져 있다면 어떻게 될까?
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);
}
}
링커에는 에러가 나지 않지만, 컴파일은 되지 않는다.
현재 함수 템플릿(std::swap)을 부분적으로 특수화해 달라고 컴파일러에게 요청한 것인데, C++는 클래스 템플릿에 대해서는 부분 특수화(partial specialization)를 허용하지만, 함수 템플릿에 대해서는 허용하지 않는다.
완전 템플릿 특수화가 안된다면 부분 특수화를 사용하면 어떨까?
함수 템플릿을 부분적 특수화 하는 방법은 오버로딩(*주 2) 버전을 추가하는 방식이 있다.
namespace std
{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //std::swap 오버로딩
{
a.swap(b);
}
}
하지만 std 내의 템플릿에 대해서는 불가능하다. std는 금단의 영역이라고 생각하면 편하다. 컴파일까진 되지만, 실행결과가 예측할 수 없다. 소프트웨어가 예상치 못한 에러를 발생시키지 않으려면 std는 절대 아무것도 추가하면 안된다.
std::swap의 특수화 버전이나 오버로딩을 하지않고 하는 방법으로 뭐가 있을까?
멤버 함수 swap을 호출하는 비멤버 함수 swap을 선언하면 해결이 가능하다.
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) //비멤버 swap 함수
{
a.swap(b);
}
이 코드로 하면 std:: 네임스페이스를 붙이면 에러가 발생한다(std::swap). 하지만 목적 자체는 이뤄냈다고 할 수 있다.
네임스페이스로 묶으면 더 깔끔하다.
namespace WidgetStuff
{
template<typename T>
class WidgetImpl { ... };
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)(*주 3)혹은 쾨니그 탐색(Koenig lookup)에 의해 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아낸다.
만약 템플릿에 대한 swap을 구현한다면 어떻게 구현해야 할까?
template<typename T>
void templateSwap(T& obj1, T& obj2)
{
swap(obj1, obj2);
}
이렇게 구현했을 때 어떤 swap을 호출하는지 생각해보자.
1.std에 있는 표준 swap
2.std의 일반형을 특수화한 버전
3.T 타입 전용의 버전
1번은 확실하게 있을 것이고, 2, 3번은 있을 수도 있고 없을 수도 있다.
만약 2, 3번이 구현되어 있지 않다면 1번이 호출되게 하는 방법으로 구현하면 될 것 같다.
template<typename T>
void templateSwap(T& obj1, T& obj2)
{
using std::swap; //std::swap을 이 함수 안으로 끌어올 수 있도록 만드는 문장
swap(obj1, obj2);
}
using std::swap을 추가함으로써 std::swap을 사용할 수 있도록 하였다. swap(obj1, obj2);를 읽을 때 컴파일러는 인자 기반 탐색으로 T 타입의 전용 swap이 있는지 찾아보고 없다면 std의 일반형을 특수화한 버전을 찾고 없다면 std의 표준 swap을 할 것이다.
이번 챕터는 표준 swap, 멤버 swap, 비멤버 swap, 특수화한 std::swap 그리고 swap 호출 시의 상황에 대해 정리했다.
정리하자면
1.표준에서 제공하는 swap이 사용에 문제가 없고 효율이 보장된다면 그냥 표준 std::swap을 사용하자
2.표준 swap의 효율성이 안좋다면 아래 대로 해보자
2-1.public 멤버 함수로 swap 이란 이름으로 만들고 이 함수는 예외를 던져서는 절대 안된다.
2-2.템플릿 혹은 클래스가 들어 있는 네임스페이스와 같은 네임스페이스에 비멤버 swap을 만든다. 그리고 2-1에서 만든 public 멤버 swap 함수를 이 비멤버 함수가 호출하도록 한다.
2-3.새로운 클래스를 만들고 있다면, 그 클래스에 대한 std::swap의 특수화 버전을 준비하자. 이 특수화 버전에서도 public 멤버 swap 함수를 호출하도록 하자.
3.사용자 입장에서 swap을 호출할 때, swap을 호출하는 함수가 std::swap을 볼 수 있도록 using 선언을 반드시 포함하자.
swap을 호출하되, 네임스페이스 한정자를 붙이지 않도록하자.
이것만은 잊지 말자
std::swap이 커스텀 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 구현하자. 이 멤버 swap은 예외를 던지지 않도록 만들자.
멤버 swap을 구현했으면, 이 멤버를 호출하는 비멤버 swap도 제공하자. 템플릿이 아닌 클래스에 대해서는 std::swap도 특수화 해두자.
사용자 입장에서 swap을 호출할 때는, std::swap에 대한 using 선언을 넣어 준 후에 네임스페이스 한정 없이 swap을 호출하자.
사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하다. 그러나 std에 어떤 것이라도 새로 추가하려고 하지 말자
(*주 1) pimpl 관용구
사용목적
1.컴파일 의존성 최소화
2.인터페이스와 구현의 분리
3.이식성
사실 요즘에 굳이 pimpl 설계로 설계할 필요는 없다. 컴파일 속도가 굉장히 빨라졌기 때문에 컴파일 성능이 눈에 띄게 올라가지 않는게 사실이다.
#include가 필요한 사용자 정의의 멤버 변수들을 해당 멤버 변수들을 포함한 클래스나 구조체의 포인터로 대체하는 기법이다.
//TEST.h
#include <vector>
#include "List.h"
class TEST
{
public:
Test();
~Test();
private:
vector<int> v;
List<int> l; //내가 구현한 list
};
헤더파일 내에서 다른 헤더파일을 include하고 있다. 헤더에서 #include 하면 전처리기는 해당 헤더의 내용을 그대로 복사해서 집어 넣는다.
결과적으로 TEST.h는 vector.h와 List.h가 모두 포함되므로 코드량이 매우 커지고 컴파일 시간이 늘어난다. 또한 vector처럼 표준 STL 라이브러리가 아니라 List.h처럼 사용자 정의 클래스인 경우 List.h가 변경되면 TEST.h도 다시 컴파일 될 수 밖에 없다.
이를 pimpl 설계로 구현한다면
//Test.h
class Test
{
public:
Test();
~Test();
private:
struct Aimpl; //선언만 한 구조체
Aimpl* pImpl; //구조체 멤버 변수
};
//Test.cpp
#include "Test.h"
#include <vector>
#include "List.h"
struct Test::Aimpl //구조체 구현
{
std::vector<int> v;
List<int> l;
};
Test::Test()
{
pImpl = new Aimpl;
}
Test::~Test()
{
delete pImpl;
}
Test클래스에서 다른 헤더에서 가져와서 사용하는 변수는 Aimpl구조체에서 선언함으로써 필요한 헤더파일을 헤더 파일에서 include하는 것이 아닌 cpp파일에서 include하고 있다.
(*주 2) 오버로딩 vs 오버라이딩
오버로딩(overloading) : 같은 이름의 메소드에서 매개변수의 개수나 유형만 다르도록 하는 기술. 템플릿 생각하면 됨
오버라이딩(overriding) : 기본 클래스의 메소드를 하위 클래스가 재정의
(*주 3) 인자 기반 탐색(argument-dependent lookup)
쾨니그 탐색이라고도 하며 이름 그대로 인수에 의존해 이름 공간을 검색하는 기능이다.
오버로드된 연산자에 대한 암시적 함수 호출을 포함하여 함수 호출 표현식에서 정규화되지 않은 함수 이름을 조회하기 위한 기능이다.
프로그래머에게 편리함을 더해주지만 찾기 힘든 버그를 발생시킬 우려가 있는것도 사실이다.
'서적 정리 > Effective C++' 카테고리의 다른 글
29.예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2021.12.19 |
---|---|
28.내부에서 사용하는 객체에 대한 "핸들"을 반환하는 코드는 되도록 피하자 (0) | 2021.12.19 |
27.캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2021.12.17 |
26.변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2021.12.17 |
24.타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2021.12.15 |
23.멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자. (0) | 2021.12.14 |
22.데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2021.12.14 |
21.함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (0) | 2021.12.14 |
댓글