자원은 사용을 마치고 나면 시스템에 돌려주어야 하는 모든 것이다.

 

 

13. 자원 관리에는 객체가 그만!

어떤 함수 내에서 팩토리 함수 등으로 객체의 생성과 삭제가 이루어진다고 했을 때, 해당 함수가 객체의 delete 문에 항상 도달한다는 보장이 없다.

팩토리 함수로 얻어낸 자원이 항상 해제되도록 하려면 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡으면 된다. 소멸자는 함수를 떠날 때 호출되도록 만들면 언제나 저절로 해제된다.

 

개발에서 사용되는 상당수의 자원은 힙에서 동적으로 할당되고 하나의 블록 또는 함수 안에서만 쓰이는 경우가 잦기 때문에 블록이나 함수를 빠져나올 때 자원이 해제되는 것이 맞다.

이 때, 스마트 포인터를 사용하면 된다.

 

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

 

자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징

 

◾ 자원을 획득한 후에 자원 관리 객체에게 넘긴다

자원 획득과 자원 관리 객체의 초기화는 한 문장에서 이루어지는 것이 일상적이다. (RAII)

 

◾ 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다

 

auto_ptr은 자신이 소멸될 때 자기가 가리키고 있는 대상에 대해서 자동으로 delete를 실행하기 때문에 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 절대로 안된다.

그래서 auto_ptr로 객체를 복사하면 원본 객체는 null로 바뀐다.

 

덧붙여서 STL 컨테이너는 auto_ptr의 원소가 될 수 없다.

 

auto_ptr을 쓸 수 없는 상황이라면 대안으로 shared_ptr가 있다.

auto_ptr과 달리 같은 자원을 여럿이 가리킬 수 있으며 참조 갯수가 0이 되면 해당 자원을 자동으로 삭제한다.

복사 동작이 의도한 대로 이루어지기 때문에 STL 컨테이너도 사용 가능하다.

단, 순환 참조가 발생하는 경우 참조 갯수가 0으로 줄어들지 않아서 자원이 해제되지 않는 문제가 발생할 수 있으므로 유의해야한다.

 

참고사항

Effective C++은 모던 C++이 적용되기 이전에 작성되었기 때문에 현재와 상이한 내용들이 있다.

auto_ptr은 C++11부터 사용 중지 권고, C++17부터 삭제되었다. 대신 unique_ptr이 등장했다.

또한 스마트 포인터의 배열의 삭제도 잘 이루어진다. (C++11/14와 17이 다르긴 하지만)

참고 : https://karupro.tistory.com/65

 

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

◾ 일반적으로 널리 쓰이는 RAII 클래스는 shared_ptr, 그리고 auto_ptr입니다. 이 둘 가운데 shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋습니다. 반면 auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버립니다.

 

 

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

모든 자원이 힙에서 생기지는 않는다.

예를 들어 Mutex가 있다. 뮤텍스 잠금을 관리하는 클래스를 만들고자 할 때 기본적으로 RAII 법칙을 따라서 구성하게 된다.

하지만 만약 이 객체의 복사가 이루어진다면?

 

◾ 복사 금지

◾ 관리 자원 참조 카운팅 수행

◾ 관리 자원을 실제로 복사 (깊은 복사)

◾ 관리하는 자원의 소유권을 옮김 (=unique_ptr)

 

위 4가지 중 하나의 선택지를 고를 수 있다.

 

◾ RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정됩니다.

◾ RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리하는 것입니다. 하지만 이 외의 방법들도 가능하니 참고해 둡시다.

 

 

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

스마트 포인터는 보통 역참조 연산자도 오버로딩하고 있기 때문에 관리하는 실제 포인터에 대한 암시적 변환도 쉽게 할 수 있다.

하지만 임의의 자원 관리 클래스를 만들어서 사용중이라면 외부에서 자원에 접근하는 방법을 제공해야 한다.

 

참고로 RAII 클래스는 데이터 은닉이 목적이 아니기때문에 자원 접근 함수를 열어두어도 캡슐화에 위배되지 않는다.

 

◾ 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다.

◾ 자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능합니다. 안전성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.

 

 

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

단일 객체의 메모리 배치구조와 객체 배열에 대한 메모리 배치구조가 다르다. 대개 배열의 경우 최상단에 원소의 개수가 같이 박혀서 들어간다.

delete 호출 시 소멸자를 한번만 호출하고 delete[] 호출 시 앞쪽의 메모리 몇 바이트를 읽어서 배열 크기라고 해석한 뒤에 해당하는 횟수만큼 소멸자를 호출한다.

 

가급적 배열 타입을 typedef 타입으로 만들지 않는 것이 좋다.

 

◾ new 표현식에 []를 썼으면, 대응되는 delete 표현식에도 []를 써야 합니다. 마찬가지로 new 표현식에 []를 안 썼으면, 대응되는 delete 표현식에도 []를 쓰지 말아야 합니다.

 

 

17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

processWidget(new Widget, priority()); // 컴파일 에러
processWidget(std::shared_ptr<Widget>(new Widget), priority()); // 컴파일 가능!.. 하지만 자원을 흘릴 가능성이 있다

 

스마트 포인터의 생성자는 explicit로 선언되어 있기 때문에 동적 할당된 포인터가 스마트 포인터 타입의 객체로 바뀌는 암시적인 변환이 존재하지 않는다.

명시적 변환으로 인자를 주게 될 경우 컴파일은 되지만 자원을 흘릴 가능성이 생긴다.

priority 호출, new Widget 실행, shared_ptr 생성자 호출 총 3가지 연산을 실행하는데 각각의 연산 순서가 컴파일러마다 다르기 때문이다.

만약 new Widget과 shared_ptr 사이에 priority 호출이 끼어들게 되고 priority 호출에서 예외 발생 시, 포인터가 유실될 수 있다.

 

해결 방법은 객체의 동적 할당과 스마트 포인터에 담는 코드를 하나의 독립적인 문장으로 만들고 스마트 포인터를 인자로 넘겨주면 된다.

 

std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

 

◾ new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만듭시다. 이것이 안 되어 있으면, 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있습니다.

'도서 > Effective C++' 카테고리의 다른 글

6. 상속, 그리고 객체 지향 설계  (0) 2022.12.04
5. 구현  (0) 2022.12.03
4. 설계 및 선언  (0) 2022.12.01
2. 생성자, 소멸자 및 대입 연산자  (0) 2022.11.30
1. C++에 왔으면 C++의 법을 따릅시다  (0) 2022.11.27

+ Recent posts