Patrick's Devlog

[Effective C++] Chapter 3 정리 본문

Study/C++

[Effective C++] Chapter 3 정리

Patrick_ 2022. 10. 27. 16:41

Item 13 : 자원 관리에는 객체가 그만

투자를 모델링하는 클래스 라이브러리를 가지고 어떤 작업을 한다고 가정하자. 이 라이브러리는 Investment라는 최상위 클래스가 있으며, 이를 기본으로 해 구체적인 형태의 투자 클래스가 파생되어 있다. 또 여기서 이 라이브러리는 Invetment에서 파생된 클래스의 객체를 사용자가 얻어내는 용도로 팩토리 함수만을 쓰도록 만들어져 있음을 가정한다. 

class Investment { .. } // 여러 형태 투자를 모델링한 최상위 클래스

Investment * createinvestment(); // Investment 클래스 계통에 속한 클래스의 객체 동적할당 및 포인터 반환
                                 // 이 객체 해제는 호출자 쪽에서 직접 해야함 (매개변수 생략)

creativeInvestment 함수를 통해 얻어넨 객체를 사용할 일이 없을 때 객체를 삭제해야 하는 쪽은 함수의 호출자이다. 아래의 코드를 살펴보자

void f() {
    Investment *pInv = createInvestment(); // 팩토리 함수 호출
    ... // pInv 사용
    delete pInv; // 객체 해제
}

f 함수는 객체를 삭제하기 위해 만들어진 함수이다. 하지만 저 함수 안에서 createInvestment 함수로부터 얻은 투자 객체의 삭제에 실패할 수 있는 경우가 허다하다. 해제를 하기 전 return 문이 들어갈수도 있으며, continue 혹은 goto에 의해 루프로부터 빠져 나올수도 있다. 여러 예외적인 상황이 오면 delete는 결국 실행되지 않게 된다. createInvestment 함수로 얻어낸 자원이 항상 해제되도록 만드는 방법은 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 해, 소멸자는 실행 제어가 f를 떠날 때 호출되도록 만들면 된다. 

흔히 스마트 포인터를 이용하면 알아서 자동으로 delete를 불러주도록 설계되어 있다. 아래의 예시를 보자.

void f() { 
    std::auto_ptr<Investment> pInv(createInvestment()); // 팩토리 함수 호출
    ... // pInv 사용 후 auto_ptr의 소멸자를 통해 삭제
}

위의 스마트 포인터를 통해 두가지 특징을 알 수 있다

 1. 자원을 획득한 후 자원 관리 객체에게 넘김 

 2. 자원 관리 객체는 자신의 소멸자를 사용해 자원히 확실히 해제되도록 함

auto_ptr은 자동으로 delete를 하므로, 두 개 이상이면 절대 안된다. 만약, auto_ptr을 쓸수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터(RCSP)가 있다. 어떤 자원을 가리키는 외부 객체 개수를 유지하다가 개수가 0이되면 해당 자원을 자동으로 삭제하는 스마트포인터이다. 이는 가비지 컬렉션의 동작과 비슷하다. RCSP의 대표적인 예는 shared_ptr이다. 

void f() {
    ...
    std::tr1::shared_ptr<Investment> pInv(createInvestment()); // 팩토리 함수 호출
    ... // 예전처럼 pInv 사용
} // shared_ptr 소멸자 통해 pInv 자동으로 삭제

이번항목에서 스마트 포인터는 항목에 대한 예시일 뿐이다. 우리가 자원해제를 하다보면 언젠간 실수를 할 것이다. 이럴 때 스마트 포인터도 하나의 좋은 방법이지만, 조금 더 우리가 신경을 써야한다. 

 

 

★ 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체 사용하자

일반적으로 널리 쓰이는 RAII 클래스는 shared_ptr, auto_ptr이다.

Item 14 : 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자

앞서 공부한 내용을 토대로 스마트 포인터를 사용하면 힙에서 생기는 자원은 처리해주지만, 힙에서 생기지 않는 자원또한 존재한다. 이럴때는 우리가 스스로 만들어야 한다 예를 하나 들어보자. 우리는 Mutex 객체를 조작하는 C API를 사용중이다. 뮤텍스는 lock과 unlock이 존재하는 것은 당연히 알고 있을 것이다. 여기서 뮤텍스 잠금을 관리하는 클래스 하나를 생성하고 싶다. 이전에 걸어놓은 뮤텍스 잠금을 잊지 않고 풀어주는 목적인 것이다. 아래의 코드를 보자

class Lock {
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm)
    { lock(mutexPtr); }
    
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};

위 코드를 사용할 때 사용자는 RAII 방식에 맞추어 쓰면 된다. 

Mutex m; // 뮤텍스 정의
...
{                  // 임계영역을 정하기 위해 블록 생성
    Lock ml(&m);   // 뮤텍스 잠금
    ...            // 임계 영역에서 할 연산 수행
}                  // 블록의 끝, 뮤텍스에 걸렸던 잠금이 풀림

위의 코드만 보면 잘 될것이지만, 만약 Lock 객체가 복사된다면 어떻게 해야할까? 

 1. 복사 금지

 2. 관리하고 있는 자원에 대해 참조 카운팅 수행

 3. 관리하고 있는 자원을 진짜로 복사

 4. 관리하고 있는 자원의 소유권을 옮김

상황에 따라 위 방법 중 적절하게 선택하면 될 것이다. 

 

RAII 객체 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가므로, 자원을 어떻게 복사하느냐에 따라 RAII 객체 복사 동작이 결정됨

RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 선으로 마무리하는 것

Item 15 : 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

앞서 진행한 Item 13에서 createInvestment등 팩토리 함수를 호출한 결과를 담기 위해 스마트 포인터를 사용하게끔 하였다. 

std::tr1::shared_ptr<Investment> pInv(createInvestment());
..

// 이때 어떤 Investment 객체를 사용하는 함수로서 우리가 사용하는 것이 다음과 같다고 가정하자
int daysHeld(const Investment *pi); // 투자금이 유입된 이후로 경과한 날수

// 그리고 이렇게 호출하고 싶을 것이다
int days = daysHeld(pInv) // 그러나 에러가 뜸

그러나 마지막 코드처럼 불러올 때, 에러가 뜨게 된다. daysHeld 함수는 Investment* 타입 실제 포인터를 원하나, 우리는 스마트 포인터 타입 객체를 넘기고 있기 때문이다. 그럼 여기서 필요한 것은 객체가 감싸고 있는 실제 자원으로 변환을 해주어야 한다. 변환 방법은 아래와 같이 두가지이다. 

1. 명시적 변환 (explicit conversion)

 -> 스마트포인터에서 명시적 변환은 get이라는 멤버 함수를 제공한다. 

2. 암시적 변환 (implicit conversion)

 

RAII 클래스를 실제 자원으로 바꾸는 방법으로 명시적 변환을 제공할 것인지, 암시적 변환을 허용할 것인지에 대한 결정은 그 RAII 클래스 만의 특정한 용도와 사용환경에 따라 달라진다. 

 

실제 자원을 직접 접근해야 하는 기존 API도 많으므로, RAII 클래스 만들 때 그 클래스가 관리하는 자원을 얻을 수 잇는 방법을 열어주어야 함

자원 접근은 명시적, 암시적 변환을 통해 가능. 안전성만 따지면 명시적 변환이 낫지만, 편의성을 생각하면 암시적 변환이 나음

Item 16 : new 및 delete를 사용할 때는 형태를 반드시 맞추자

우리가 new 연산자를 사용해 표현식을 꾸미게 되면(동적할당), 이로 인해 두가지 내부 동작이 진행된다. 우선 메모리가 할당된다. 그 이후 할당된 메모리에 대한 한 개 이상의 생성자가 호출 된다. delete를 사용하게 될 경우, 기존에 할당된 메모리에 대해 한 개 이상의 소멸자가 호출되고, 그 후에 메모리가 해제된다. 여기서, delete 연산자가 적용되는 객체는 몇개나 될까? 이것이 바로 소멸자가 호출하는 횟수가 된다.

쉽게 질문을 풀어쓰자면, 삭제되는 포인터는 객체 하나만 가리키는 것이 아닌 객체의 배열을 가리킨다. 포인터의 동적할당을 해제하기 위해 delete를 쓸때, 대괄호 쌍을 delete 뒤에 붙여주면 delete 또한 객체가 배열임을 알게된다. 그러나, 아무것도 해주지 않으면 이는 단일 객체로 인지하게 된다.

std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
..
delete stringPtr1;   // 객체 한 개 삭제
delete[] stringPtr2; // 객체 배열 삭제

★ new에 []를 쓰면 delete에도 []를 써야하며, 쓰지 않았으면 delete에도 쓰지 말아야 함

'Study > C++' 카테고리의 다른 글

[Effective C++] Chapter 2 정리 - 2  (1) 2022.09.02
[Effective C++] Chapter 2 정리 - 1  (0) 2022.05.26
[Modern C++] Lvalue 및 Rvalue  (0) 2022.05.20
[Modern C++] Smart Pointer  (0) 2022.05.17
[Modern C++] 시작하기  (0) 2022.05.16