본문 바로가기

Effective C++/3. 자원 관리

항목 13: 자원 관리에는 객체가 그만!

  • 만약 투자를 모델링 해주는 클래스 라이브러리를 가지고 어떤 작업을 한다고 생각해보자. 라이브러리는 Investment라는 최상위 클래스가 있고. 이것을 기본으로 구체적인 형태의 투자 클래스가 파생되어 있다. 라이브러리는 Investment에서 파생된 클래스의 객체를 사용자가 얻어내는 용도로 팩토리 함수(항목 7 참조)만을 쓰도록 만들어져 있다고 해보자.

 

 

 

객체의 해제는 사용자가 직접 해야 한다고 가정하면, 저렇게 delete pInv; 것인데 당연히 f 실행되면 delete문이 실행될 것이라고 믿으면 안됨

 

처음엔  무조건 delete문으로 코드가 흐르게 해놨어도, 유지보수를 하면서 멍청한 다른 프로그래머가 continue return 남발한다던지 해서 흐름을 박살냈을 수도 있다. 이때 delete문이 실행이 안되면? f 끝나도 객체가 메모리상에 그대로 남아있다. -> 바로 자원이 줄줄센다. (자원은 다썻으면 무조건 해제해야됨)

 

createInvestment()으로 얻어낸 자원이 항상 해제되도록 만들 방법

: 자원(우리가 new 할당한 메모리) 특정 객체에 넣고, 자원의 해제를 객체의 소멸자가 맡도록 하고, 소멸자는 f 종료될 자동으로 호출되게 하는 것이다.

 

이런 용도로 쓰라고 만들어진 객체가 있는데 바로 auto_ptr 이다. 놈은 포인터랑 비슷하게 동작하는 스마트 포인터라는 놈인데, 만약 auto_ptr obj라는 객체를 가르키고 있다면, auto_ptr 소멸될 소멸자가 자동으로 delete obj; 해준다.

 

참고) vs에서 tr1 사용방법

 

 

여기서 중요한 포인트

 

  1. 자원을 획득한 자원 관리 객체(auto_ptr)에게 넘긴다.

 

예제를 보면 createInvestment() 만들어 자원은 자원을 관리할 auto_ptr 객체를 초기화 하는데 쓰인다.

=> 자원 획득 초기화(Resource Acquisition Is Initailization, RAII)

=> 자원 획득과 동시에( 문장 안에) 자원관리 객체의 초기화가 이루어 지는건 너무 당연한 것이기 때문

가끔씩 자원 관리 객체를 초기화 상태에서 자원을 객체에 대입하는 경우도 있긴 한데 자원을 획득한 바로 자원 관리 객체에 넘겨준다. 라는 사실은 같다.

 

 

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

 

 

auto_ptr 자신이 소멸될 자신이 가르키고 있는 대상에 대해 자동으로 delete 호출하기 때문에, 어떤 객체를 가르키는 auto_ptr 두개 이상이면 절대로 안된다. 만에 하나 이런 사태가 발생했다면, 결국 자원이 삭제되는 결과가 되고 이는 미정의 동작(logical error) 일어날 있다.

 

이런 불상사를 막기 위해 auto_ptr 유별난 특징이 있다.

auto_ptr 객체를 복사하면(복사 생성자 or 복사 대입 연산자) 원본 객체는 null(pointer) 만든다. 가르키는 자원을 소유하는 객체는 무조건 하나여야 한다는 것이다.

 

참고) 스마트포인터종류

 

auto_ptr C++17 기준 제거됨…

 

또한 위의 예제와 같이 auto_ptr은 값 단위 복사를 할 수 없습니다. 단지 소유권 이동입니다.

위의 상황때문에 auto_ptr은 사용 중지 권고가 됐고, 대신 더 강력하고 auto_ptr을 대체할 unique_ptr이 등장했습니다.

 

출처: <https://openmynotepad.tistory.com/33>

 

 

 

 

auto_ptr 단점이라면, 일단 복사 방식이 관례를 벗어난다. 그리고 auto_ptr 관리하는 객체는 이상의 auto_ptr 객체가 물고 있으면 안된다는 요구사항까지 깔려 있다.

=> 모든 동적 할당 자원에 auto_ptr 붙이긴 힘들 것이다. 예를 들어 STL 컨테이너의 경우엔 원소들이 '정상적인' 복사 동작을 해야 하기 때문에, auto_ptr 이들의 원소로 허용되지 않는다.

 

 

auto_ptr 없는 상황이라면, 대안으로 참조 카운팅 방식 스마트 포인터(reference-counting smart pinter: RCSP) 아주 좋다.

 

RCSP : 특정한 어떤 자을 가리키는(참조하는) 외부 객체의 개수를 유지하고 있다가, 개수가 0 되면 해당 자원을 자동으로 삭제하는 스마트 포인터이다. <- Garbage Collection 상당히 유사

 

, 참조 상태가 고리를 이루는 경우(예를 들어 다른 객체가 서로를 가르키고 있는 경우) 없앨 없다는 점은 가비지 컬렉션과 다르다. 아직 말인지 모르겠다.

 

TR1에서 제공되는 tr1::shared_ptr (항목 54 참조) 대표적인 RCSP 이다. 이것을 사용하여 f 작성하면

 

 

auto_ptr 사용방법이 유사, 하지만 shared_ptr 복사를 때를 보면

 

 

auto_ptr 다르다. 복사 동작이 관례대로 이루어지기 때문에, auto_ptr 괴랄한 복사 방식 때문에 못썼던 STL 컨테이너 등을 사용할 있다.

 

여튼 중요한

 

자원 관리 객체를 사용하여 자원을 관리하는

 

알아야 것이 하나 있다. auto_ptr shared_ptr 소멸자에서 delete 연산자를 사용한다. delete[] 아니다!!

 

 

둘의 차이는 항목 16에서 참고하고, 결과만 말하면 동적으로 할당한 배열에 대해 auto_ptr이나 shared_ptr 쓰면 난감하다.

 

syntax error 아닌, run-time error 이다!!

 

참고로 동적 할당된 배열을 위한 스마트 포인터는 표준 라이브러리에는 없다!!

왜냐하면 동적 할당된 배열은 vector string으로 거의 대체할 있기 때문이다.

 

굳이 동적 할당된 배열에 원하면, 부스트에 boost::scoped_array boost::shared_array 있긴 하다.(항목 55 참조)

 

다시 강조

 

자원 관리에는 자원 관리 객체를 쓰자. 우리가 자원 해제를 일일이 하면 언젠가 사고가 나게 되있다.

auto_ptr이나 shared_ptr 쓰는 것도 방법인데, 이들이 관리 못하는 자원(대표적으로 동적 할당된 배열) 있으니 알아두자.

 

마지막으로 한마디

 

createInvestment() 리턴 타입이 포인터인데, 이게 문제가 발생할 있다. 반환된 포인터에 대한 delete 사용자측에서 해야하는데, 그것을 잊어버리고 넘어갈 있기 때문이다.(스마트 포인터를 사용한다 해도 createInvestment() 리턴값을  스마트 포인터에 저장해야 한다는 사실을 알아야 한다.)

 

항목 18 createInvestment() 인터페이스를 수정하는 것을 올림

 

  • 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
  • 일반적으로 널리 쓰이는 RAII 클래스는 str::shared_ptr 이다. (auto_ptr 삭제됨)