항목 13, 14에서 배운 것을 토대로 자원 관리 객체를 사용한다면, 직접 자원에 접근하지 않으면서 자원 누출에 대한 걱정을 지울 수 있다.
그러나 수많은 API들은 자원을 직접 참조하도록 만들어져 있다.
결국 자원 관리 객체의 보호를 넘어 실제 자원에 직접 접근해야 할 일이 생기는 것이다.
// 항목 13
std::shared_ptr<Investment> pInv(createInvestment()); // createInvestment()는 팩토리 함수
int daysHeld(const Investment *pi); // 인자로 Investment*를 원함
int days = daysHeld(pInv); // Error!
위 코드는 컴파일 에러가 발생한다. Investment*
가 아닌 shared_ptr<Investment>
타입의 객체를 넘겼기 때문이다.
RAII 클래스의 객체(여기서는 shared_ptr<Investment>
)를 그 객체가 감싸고 있는 실제 자원(Investment*
)으로 변환해야 한다.
int days = dayHeld(pInv.get()); // 실제 포인터(Investment*)를 넘기므로 에러 X
unique_ptr와 shared_ptr는 명시적 변환을 수행하는 멤버함수 get
을 제공한다.
get
을 통해 스마트 포인터 객체에 들어있는 실제 포인터의 사본을 얻을 수 있다.
class Investment {
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); // 팩토리 함수
std::shared_ptr<Investment> pi1(createInvestment()); // shared_ptr가 자원 관리 수행
bool taxable1 = !(pi1->isTaxFree()); // operator->를 사용하여 자원에 접근
...
std::unique_ptr<Investment> pi2(createInvestment()); // unique_ptr가 자원 관리 수행
bool taxable2 = !((*pi2).isTaxFree()); // operator*를 사용하여 자원에 접근
unique_ptr와 shared_ptr는 포인터 역참조 연산자(operator->
, operator*
)도 오버로딩하여 제공한다.
// C API가 제공하는 함수
FontHandle getFont();
void releaseFont(FontHandle fh); // 폰트 자원 해제 함수
void changeFontSize(FontHandle f, int newSize); // 기타 폰트 조작 함수
C API로 직접 조작 가능한 FontHandle
객체를 보호하기 위해 RAII 클래스인 Font
클래스를 직접 만들어보자.
// RAII 클래스
class Font {
public:
explicit Font(FontHandle fh):f(fh) {} // 자원 획득! 자원 해제를 C API로 하기 때문에 pass by value로 넘김
~Font() { releaseFont(f); } // 자원 해제!
private:
FontHandle f; // 실제 폰트 자원
}
기존에 있는 C API는 FontHandle
을 사용하도록 만들어져 있으므로 Font
객체를 FontHandle
로 변환해야 할 경우가 종종 있을 것이라 예상할 수 있다.
class Font {
public:
...
FontHandle get() const { return f; } // 명시적 변환 함수
...
}
이제 get
을 통해 명시적으로 Font
에서 FontHandle
로 변환할 수 있다.
Font f(getFont()); // Font 객체 생성자 호출
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // 명시적으로 변환 후 넘김
그러나 변환할 때마다 get
함수를 호출해야 한다.
class Font {
public:
...
operator FontHandle() const { return f; } // 암시적 변환 함수
...
}
암시적 변환 함수를 제공하므로 이제는 명시적으로 get
을 사용하지 않아도 Font
에서 FontHandle
로 변환할 수 있다.
Font f(getFont()); // Font 객체 생성자 호출
int newFontSize;
...
changeFontSize(f, newFontSize); // 암시적으로 변환 후 넘김
하지만 '번거로움이 줄었으니 암시적 변환 함수를 만들어야지' 라고 단순하게 생각하면 안된다.
정말로 Font
를 사용하려고 했는데 암시적 변환 함수 때문에 FontHandle
로 바뀌어버릴 수 있기 때문이다.
Font f1(getFont());
...
FontHandle f2 = f1; // f1이 관리하고 있던 FontHandle 객체가 f2로 복사되었다.
이제 f2
를 통해서 f1
이 관리하고 있는 폰트(FontHandle
)를 직접 사용할 수 있게 되었다.
그러나 하나의 자원을 두 객체에서 사용할 수 있는 상황은 그다지 좋은 상황이 아니다.
f1
이 소멸되는 시점(블락이나 함수가 끝남)부터 FontHandle
객체는 해제될 것이고, f2
는 이미 해제된 폰트를 잡고 있게 되는 것이다.
참고: 사용자 정의 타입변환과 explicit
암시적 변환에 대해 더 알고 싶다면 가볍게 볼 만한 포스팅인 것 같다.
정답은 없다. RAII 클래스를 실제 자원으로 바꾸는 방법으로 명시적 변환을 제공할 것인지, 암시적 변환도 제공할 것인지는 해당 RAII 클래스의 용도와 사용 환경에 따라 다르다.
단, 대체적으로는 get
등의 명시적 변환 함수만들 제공하는 것이 좋다.
이번 항목에서 RAII 클래스의 실제 자원을 참조하는 방법들에 대해 배우면서 캡슐화를 위반하는 것이 아닐까 라는 의문이 들었을 수 있다.
틀린 말은 아니지만, 애초에 RAII 클래스의 목적은 정보 은닉이 아니라 자원 해제이기 때문에 틀린 설계도 아니다.
캡슐화를 하고 싶다면, 자원 해제라는 기본 기능 위에 얹을 수는 있다.
실제로 shared_ptr의 경우 참조 카운팅 메커니즘에 필요한 장치들은 모두 캡슐화하지만, 실제 자원에는 쉽게 접근이 가능하다.
정리
1. 실제 자원에 접근해야 하는 기존 API들이 많기 때문에 RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법(명시적/암시적 변환)을 제공해야 한다.
2. 안정성으로는 명시적 변환이, 고객 편의성으로는 암시적 변환이 낫다.