센로그

[EC++] 13. 자원 관리에는 객체가 그만! 본문

Effective/Effective C++

[EC++] 13. 자원 관리에는 객체가 그만!

seeyoun 2024. 11. 18. 23:31

delete는 항상 호출될 것인가?

class Investment { ... };

Investment* createInvestment();		// Investment 계통에 속한 클래스의 객체를 동적할당하고 그 포인터를 반환

void f() {
    Investment* pInv = createInvestment();
    ...
    delete pInv;
};
  • 이 코드에서 delete pInv; 가 항상 호출될 것이라고 믿으면 안된다.
    • ... 부분에 return 문이 있다면?
    • ... 부분에서 예외가 던져진다면?
  • delete 문이 실행되지 않게 된다면, 객체를 담고 있는 메모리가 누출되며, 객체가 갖고 있던 자원도 모두 누수된다.

 


자원이 항상 해제되도록 만들 수는 없을까?

  • 자원을 객체에 넣고, 그 자원 해제를 소멸자가 맡도록 하며, 소멸자는 로컬 스코프를 떠날때 호출되도록 만들면 된다.

 


예시 상황

(C++ 17 이후로 auto_ptr은 사라졌지만, 일단 책에서 제시한 예제를 그대로 사용하기로 함.)

 

만약 위 코드에서 pInv 포인터를 std::auto_ptr로 만들면 어떨까?

  • 이 방법에는 다음 두가지 특징이 있다.
    • 자원을 획득하고 나서 바로 자원 관리 객체에게 넘겨준다.
      • RAII: 자원의 획득은 객체의 초기화와 동일하다. (Resource Acquisition Is Initialization)
      • 자원을 소유하고 관리하는 객체를 생성할 때 해당 자원을 초기화하고, 객체가 소멸될 때 자원을 자동으로 해제하도록 설계하는 방식이다.
    • 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
      • 객체는 스코프가 끝나면 소멸되므로, 자원은 스코프를 벗어나면 자동으로 해제된다.
      • 즉, 실행 제어가 어떤 경위로 스코프를 떠나는가에 상관 없이 자동으로 자원 해제가 이루어진다.

 

auto_ptr을 쓰기 힘든 상황이라면 shared_ptr을 고민해봐도 좋다.

  • 참조 카운팅 방식으로, 참조가 0이 되면 자원을 해제한다.

 


스마트 포인터로 배열 다룰 때 주의할 점

  • 예전에는 동적 배열을 스마트포인터로 가리키면 안됐다. 
    • 소멸자에 정의된 delete가 delete[]가 아니었기 때문이었다.
  • 근데 요즘은 되는 듯.
    • 커스텀 소멸자 정의할 수 있다. 거기다가 delete[]로 넣으면 된다.
    • 또는, 애초에 타입 받을 때 배열 형태(T[]) 로 받으면 된다.

 


추가

+) auto_ptr vs unique_ptr

std::unique_ptr은 std::auto_ptr의 기능을 현대적이고 안전하게 대체하기 위해 설계되었다.

주요 차이점은 다음과 같다:

 

+) unique_ptr이 상대적으로 스레드세이프한 이유는?

  • auto_ptr의 경우 복사 시 소유권이 전이되는 형태이며, 암묵적으로 소유권이 전이된다.
    • 복사가 허용되면, 여러 스레드에서 동일한 auto_ptr을 복사하려고 할 때 Race Condition이 발생하기 매우 쉽다.
  • unique_ptr은 std::move를 통해 명시적으로 소유권을 전이한다.
    • 여전히 Race Condition이 발생할 수 있지만, 명시적인 이동 설계 덕분에 이를 더 쉽게 관리할 수 있다.
  • 물론 둘 다 완전히 스레드세이프하지는 않다. 그러나 unique_ptr이 좀 더! 낫다는 거다.

 

+) 왜 std::auto_ptr에서 std::unique_ptr로 바뀌었는가?

  1. 복사 시 소유권 전이의 문제:
    • std::auto_ptr은 복사 시 소유권이 이동되기 때문에, 예기치 않은 동작을 유발할 수 있다.
      • 이는 코드의 직관성을 떨어뜨리고, 의도하지 않은 메모리 해제를 초래할 수 있다.
    • std::unique_ptr은 복사를 금지하고, 소유권 이동만 허용함으로써 이 문제를 해결했다.
  2. 스레드 안전성:
    • std::auto_ptr은 멀티스레드 환경에서 사용하기에 적합하지 않았다.
    • std::unique_ptr은 이동 시점을 명확히 지정하고, 불필요한 동기화 문제를 줄였다.
  3. 커스터마이징 가능성:
    • std::unique_ptr은 커스텀 해제 함수(Deleter)를 지정할 수 있어 더 유연하다.
     
    std::unique_ptr<int, void(*)(int*)> ptr(new int(42), [](int* p) { std::cout << "Deleting " << *p << std::endl; delete p; });
  4. 현대적인 메모리 관리 패턴과의 호환성:
    • C++11에서 도입된 이동 시맨틱(move semantics)와 더 잘 어울린다.

 

Comments