Effective C++/Chapter 3: 자원관리

[Effective C++] 항목 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

yeoul0714 2025. 4. 29. 18:25

1. 개요


자원 관리 클래스는 메모리 누수를 막는 아주 훌륭한 방법입니다.

 

그러나 우리는 종종 자원 관리 객체의 보호벽을 넘어서 실제 자원을 직접 만져야 할 일이 종종 생길것입니다.

 

std::tr1::shared_ptr<Yeoul> yptr(createYeoul());

int YeoulAge(const Yeoul* ptr);

int age = YeoulAge(yptr); // 에러

 

만약 코드를 이렇게 작성한다면 오류가 발생합니다.

 

왜냐하면 shared_ptr로 선언된 yptr과 Yeoul의 포인터는 형식이 맞지 않기 때문이지요

 

우리는 shared_ptr<Yeoul>을 Yeoul*로 변환활 방법을 필요로 합니다.


 

2. 1번의 문제 해결법


2-1. 명시적 형변환(explicit conversion)

tr1::shared_ptr은 명시적으로 raw pointer를 가져올 수 있도록 get이라는 멤버 함수를 제공합니다.

 

get을 통해서 스마트 포인터 객체의 원본 포인터의 사본을 얻어낼 수 있습니다.

 

그러므로 방식을 써서 위의 오류를 해결한다면

int age = YeoulAge(yptr.get());

 

이런식으로 get을 써주면 해결됩니다.

 

2-2. 연산자 overload사용

std::tr1:shared_ptr은 자체적으로 역참조 연산자(->, *)를 오버로딩 하고있습니다.

 

그러므로 암시적 변화도 쉽게 할 수 있습니다.

 

아래가 그 예제입니다.

 

*와 ->를 이용해 raw pointer에 접근하는 모습입니다.

class Investment {  // 투자 유형 계층 구조의 루트 클래스
public:
   // 세금 면제 여부를 확인하는 함수
   bool isTaxFree() const;
   ...
};

// 팩토리 함수
Investment* createInvestment();

// tr1::shared_ptr로 자원 관리
std::tr1::shared_ptr<Investment> pi1(createInvestment());
// operator->를 통해 자원에 접근
bool taxable1 = !(pi1->isTaxFree());
...

// auto_ptr로 자원 관리
std::auto_ptr<Investment> pi2(createInvestment());
// operator*를 통해 자원에 접근
bool taxable2 = !((*pi2).isTaxFree());

 

2-3. 암시적 형변환(implicit conversion)

FontHandle getFont();     // C API에서 제공 - 간결함을 위해 매개변수 생략됨
void releaseFont(FontHandle fh);  // 동일한 C API에서 제공

class Font {  // RAII 클래스
public:
    // 자원 획득: 명시적 생성자
    explicit Font(FontHandle fh)  // 값으로 전달 (C API 특성상)
        : f(fh)  
    { }
    
    // 자원 해제: 소멸자에서 자동으로 처리
    ~Font() { 
        releaseFont(f); 
    }
    
    // 복사 관련 처리는 Item 14 참조
    ...
    
private:
    FontHandle f;  // 원시(raw) 폰트 자원
};

 

들어가기전 Font클래스는 shared_ptr처럼 자원 관리 클래스입니다.

 

Font클래스가 관리하는 자원의 타입은 FontHandle이라는 점을 인지하고 읽으시면 이해에 도움이 될것입니다.

 

shared_ptr가 관리하는 것은 raw pointer였다면 Font는 FontHandle인것입니다.

 

이러한 클래스가 있을 때 FontHandle로 변환해서 사용해야 하는 경우가 매우 많다고 생각해보겠습니다.

 

그러면 Font클래스에서 명시적으로 변환함수를 만들어도 될것입니다.

class Font {
public:
...
FontHandle get() const { return f; } //명시적 변환 함수
};

이렇게 만들어두면 필요할때마다 get을 써도 되지만 매번 호출하는 것이 매우 번거로울지도 모릅니다.

 

void changeFontSize(FontHandle f, int newSize); // 
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // Font에서 FontHandle로
									  // 명시적 변환후 넘겨줌

너무나 귀찮아서 get을 쓰지 자원관리 클래스인 Font클래스를 쓰지 않겠다고 할지도 모릅니다.

 

그러나 메모리 누수가 발생한다면 매우 슬플것입니다.

 

그래서 대안이 있습니다.

 

FontHandle로 변환시키는 암시적 변환 함수를 Font에서 제공하는 것입니다.

class Font {
public:
...
    operator FontHandle() const // 암시적 변환 함수
    { return f; }
...
};

 

이 부분은 타입 변환 연산자 입니다.

 

이 개념이 없으면 책을 이해하기 어려운 부분이 있을것입니다.

타입 변환 연산자란?

타입 변환 연산자는 객체를 다른 타입으로 변환하는 방법을 정의하는 특수한 멤버 함수입니다.

 

아래 처럼 사용합니다.

operator 타입이름() const { /* 변환 코드 */ }

 

호출되는 상황

이 연산자는 다음과 같은 상황에서 자동으로(암시적으로) 호출됩니다.

 

1. 함수 인자로 전달될 때

void changeFontSize(FontHandle fh, int size);

Font myFont(getFont());
changeFontSize(myFont, 12);  // 여기서 연산자 호출됨

컴파일러는 myFont를 FontHandle 타입으로 변환하기 위해 myFont.operator FontHandle()을 자동으로 호출합니다.

 

2. 변수 대입 시

Font myFont(getFont());
FontHandle handle = myFont;  // 여기서 연산자 호출됨

myFont가 FontHandle 타입의 변수에 대입될 때, 변환 연산자가 호출됩니다.

 

이러한 개념을 인지하고 읽으시면 좀더 이해가 수월합니다.

 

Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // Font > FontHandle로
  								// 암시적 변환 수행

이렇게 get을 쓰지 않고 바로 넘길 수 있습니다.

 

기존의 코드(아래)와 비교되는 부분입니다.

void changeFontSize(FontHandle f, int newSize); // 
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // Font에서 FontHandle로
									  // 명시적 변환후 넘겨줌

 

 

Font f1(getFont());
...
FontHandle f2 = f1;

이렇게 쓰면 Font 객체를 복사하려고 시도했지만 f1이 FontHandle로 바뀌고 복사됩니다.

 

이렇게 되면 Font객체인 f1이 관리하는 FontHandle이 f2를 통해서도 사용할 수 있게 됩니다.

 

이런 상황은 절대 좋지 못합니다.

 

그리고 f1이 소멸될때 f2는 해제된 f1에 매달려 있는 꼴이 됩니다. (dangling pointer)


 

3. 결론


RAII클래스를 실제 자원으로 바꾸는 방법으로 명시적/암시적변환 어떤것을 쓸지는 용도와 조건에 따라 달라집니다.

 

보통 암시적 변환보다 명시적으로 get과같은 함수를 이용해 변환하는 것이 좋을때가 많습니다.

 

원하지 않는 타입 변환을 줄여주기 때문이죠

 

그러나 암시적 타입변환으로 생기는 자연스러움도 큰 장점이긴 합니다.

 

RAII에서 자원에 접근하는 방식이 캡슐화를 위반한다고 생각할지도 모릅니다.

 

그러나 RAII클래스는 캡슐화를 목적으로 하지 않습니다.

 

이 클래스는 자원관리를 용이하게 하기위함을 목적으로 하고 있습니다.

 

그러므로 굳이 이런부분은 걱정하지 않아도 됩니다.

 

shared_ptr같은 경우엔 reference counting에 필요한 장치들은 전부 캡슐화로 숨기고 있지만

 

포인터에 쉽게 접근하는 통로 역시 남겨두었습니다. (느슨한 캡슐화, 엄격한 캡슐화)

 

이것만은 잊지 말자

  • 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다.
  • 자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능합니다. 안전성만 따지면 명시적 변환이 보통 낫지만
    고객 편의성을 놓고 보면 암시적 변환이 좋습니다.

https://yeoul0714.tistory.com/53

 

[Effective C++] 항목 16: new 및 delete를 사용할 때는 형태를 반드시 맞추자

1. 개요std::string *stringArray = new std::string[100];delete stringArray; 이 코드는 얼핏 보기에는 아무런 문제가 없어 보입니다. 메모리를 동적할당받고 잘 삭제해주고 있으니까요 그러나 이것은 완벽히 잘못

yeoul0714.tistory.com