- 만약 투자를 모델링 해주는 클래스 라이브러리를 가지고 어떤 작업을 한다고 생각해보자. 이 라이브러리는 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 사용방법
여기서 중요한 포인트
- 자원을 획득한 후 자원 관리 객체(auto_ptr)에게 넘긴다.
예제를 보면 createInvestment()가 만들어 준 자원은 그 자원을 관리할 auto_ptr 객체를 초기화 하는데 쓰인다.
=> 자원 획득 즉 초기화(Resource Acquisition Is Initailization, RAII)
=> 자원 획득과 동시에(한 문장 안에) 자원관리 객체의 초기화가 이루어 지는건 너무 당연한 것이기 때문
가끔씩 자원 관리 객체를 초기화 안 한 상태에서 자원을 그 객체에 대입하는 경우도 있긴 한데 자원을 획득한 후 바로 자원 관리 객체에 넘겨준다. 라는 사실은 같다.
- 자원 관리 객체는 자신의 소멸자를 사용해서 확실히 해제되도록 한다.
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은 삭제됨)
'Effective C++ > 3. 자원 관리' 카테고리의 다른 글
항목 17: new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자. (0) | 2021.04.24 |
---|---|
항목 16: new 및 delete를 사용할 때는 형태를 반드시 맞추자. (0) | 2021.04.24 |
항목 15: 자원 관리 클래스에서 관리하는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2021.04.24 |