ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 3장(1) - 자원 관리
    쾌락없는 책임 (공부)/Effetive C++ 요약본 2022. 3. 11. 14:50
    반응형
    본 카테고리는 프로텍 미디어의 '이펙티브 C++'을 보고 요약하는 카테고리입니다.

    3판을 기준으로 하며 전체 내용이 아닌 간략한 내용만을 요약하고 있습니다.

     

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

    void f(){
    	Investment *p = createInvestment();
    	...
    	delete p;
    }

     팩토리 함수에서 객체를 만들어 사용한 뒤 delete를 하는 모습인데 만일 delete가 실행되지 않는다면 어떨까요? 위 ... 에서 예외가 있다던가 continue가 있었다던가 등등 여러 이유에서 말이죠. 프로그램을 만들때 이런 사항들에 잘 대응해야겠지만 유지보수 측면에서 이런 상황들을 배제 할 필요가 있습니다.

     

     그래서 나온 방법이 자원을 객체에 넣고 그 자원 해제를 소멸자가 하게 하는 것입니다!

     

     자원들 대부분이 힙에서 동적으로 생성이 되며 하나의 블럭( {} 안 )에서 사용되는 일이 잦습니다. 해당 블럭을 나오게 되면 사라진다는 것을 이용해서 객체 관리를 하는 것이죠. 그리고 이를 위해서 나온게 스마트 포인터입니다!

    void f(){
    	std::auto_ptr<Investment> p(createInvestment());
    }

    (책이 옛날이라 auto_ptr을 사용하고 있는데 auto_ptr은 C++11이후로 사라지게 되었습니다)

     위 코드를 보면 따로 delete 없이 자원 관리 객체에서 자동으로 소멸자를 불러 줍니다! 그러면 소멸자에서 delete를 함으로서 메모리 관리를 해주는 것이죠. 그리고 위 자원 관리 객체를 사용하는 방법중 중요한 2가지 방법이 있습니다.

     

    1. 자원 흭득 후 자원 관리 객체에게 넘기기 (자원 흭득 후 초기화 - RAII)

       위 예제에서 함수가 만들어낸 자원을 auto_ptr 객체를 초기화하는데 사용하고 있습니다.

     

    2. 자원 관리 객체는 자신의 소멸자를 사용해 자원을 확실히 해제한다

       소멸자가 불리는 와중 예외가 호출되는 경우가 있는데 이는 항목 8을 참고해야 합니다.

     

    [Effective C++] 2장(1) - 생성자, 소멸자 및 대입 연산자

    본 카테고리는 프로텍 미디어의 '이펙티브 C++'을 보고 요약하는 카테고리입니다. 3판을 기준으로 하며 전체 내용이 아닌 간략한 내용만을 요약하고 있습니다. 항목 5 : C++가 은근슬쩍 만들어 호

    husk321.tistory.com

     

     그리고 위 auto_ptr은 가리키고 있는 대상에 대해 자동으로 delete를 하기 때문에 객체를 가리키는 포인터의 수가 2개 이상이면 안됩니다! 만약 한쪽이 블럭을 벗어나면 그 객체가 delete되어 다른쪽이 붕 떠버리니깐요. 그래서 auto_ptr은 복사할 시 원복 객체를 null로 만들어 버립니다. 이로 인해 모든 동적 생성 자원에 사용하면 안됩니다. 그래서 STL은 auto_ptr을 허용하고 있지 않습니다.

     

     위 문제로 인한 대안으로 나온게 tr1::shared_ptr이며 이는 참조 카운팅 방식 스마트 포인터(Reference-counting smart pointer - RCSP) 로 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하다가 그 개수가 0이 되면 자원을 삭제해줍니다.

     

     뭐 스마트 포인터는 이 외에도 여러가지가 있으니 따로 알아보도록 하고 결국 이번 장의 핵심 주제는 자원을 관리하는 객체를 사용해 자원을 관리하자 입니다.

     

    + 추가적으로 스마트 포인터들을 내부에서 delete를 사용하므로 동적 배열을 할당하고 싶으면 vector, string 등으로 해결합시다.

     

     

     

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

     바로 위에서 자원 관리 객체를 살펴봤지만 모든 자원이 힙에서 생기지는 않습니다. 그리고 힙에서 생기지 않은 자원은 스마트 포인터로 처리하기 곤란해집니다. 이럴 때는 자원 관리 클래스를 스스로 만들어야 합니다.

     만약 뮤텍스 객체를 조작하는 C API를 사용한다고 할 때 객체 소멸시 잠금을 해제하고 싶습니다.

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

     이제 사용자는 위 RAII 방식에 맞게 사용을 하면 되지만 문제가 있다면 Lock 객체가 복사될 때 입니다. 그래서 이런 RAII 객체가 복사될 때 어떤 동작을 해야 하는지를 알아둬야 합니다. 그리고 이 책에서 열의 아홉은 사용하는 방식들이 나와 있습니다.

     

    < 복사를 금지 >

     애초에 이 객체가 복사되는 경우가 많습니다. 그래서 복사 함수를 private로 지정해서 복사가 되지 않게 해야합니다.

     

    < 관리하고 있는 자원에 대해 참조 카운팅을 수행 >

    위 클래스에서 shared_ptr을 멤버로 넣어주면 됩니다.

    class Lock{
    public:
    // shared_ptr을 초기화하는데 가리킬 데이터로 Mutex 객체의 포인터를 사용
    // 삭제자로 unlock 함수를 사용
    	explicit Lock(Mutex *pm)
    		: mutexPtr(pm, unlock);
    	{
    		lock(mutexPtr.get()
    	}
    
    private:
    	std::tr1::shared_ptr<Mutex> mutexPtr;   // 원시 포인터 대신 이거
    };

     단순 원시 포인터 Mutex*에서 shared_ptr로 변경이 되었습니다. 그리고 이 스마트 포인터는 참조 카운트가 0이 될 때 불리는 함수인 삭제자(deleter)를 지정해 줄 수도 있습니다!

     

    < 관리하고 있는 자원을 진짜 복사하기 >

     실제로 원하는대로 복사하는 경우도 있습니다. 이때는 '자원을 다 썼을 때 각각의 사본을 확실히 해제' 하는 것이 이 클래스의 존재 명분이 됩니다. 그리고 이런 경우 객체가 둘러싸는 자원까지 복사를 하는 깊은 복사를 수행해야 한다는 점입니다.

     string의 경우에도 복사를 하면 새로운 포인터와 새로운 힙 영역을 가지게 됩니다.

     

    < 관리하는 자원의 소유권을 옮김 >

     auto_ptr과 동일한 방법이지만 흔하게 사용하는 방법은 아닙니다.

     

     

     

     

    항목 15 : 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있게 하자

     자원 관리 클래스는 정말 좋지만 이걸로 모든 문제가 해결되는건 아닙니다. 실제 코드를 짤 때 실제 자원을 만져야 하는 경우가 있습니다.

    std::tr1::shared_ptr<Investment> pInv(createInvestment());
    ...
    int daysHeld(const Investment *pi);
    ...
    int days = daysHeld(pInv);

     위 코드는 컴파일 에러가 나오게 됩니다. 함수가 원하는건 원시 포인터인데 인자는 스마트 포인터가 들어갔기 때문이죠. 그래서 RAII 클래스의 객체를 실제 자원으로 변환할 필요가 있는데 이때 생각나는 방법이 명시적 변환, 암시적 변환인 것이죠.

     

     스마트 포인터들은 명시적 변환을 수행하는 get 함수를 제공하고 있으며 이를 통해서 실제 포인터를 얻어낼 수 있습니다.

    int days = daysHeld(pInv.get());

     그리고 제대로 만들어진 스마트 포인터들의 경우 포인터 역참조 연산자 (operator -> , operator *)도 지원하고 있습니다. 그래서 암시적 변환도 가능하죠.

    class Investment{
    public:
    	bool isTaxFree() const;
    	...
    };
    
    Investment* createInvestment();   // 팩토리 함수
    
    std::tr1::shared_ptr<Investment> pil(createInvestment());
    
    bool taxable1 = !(pil->isTaxFree());
    
    ...
    std::auto_ptr<Investment> pi2(createInvestment());
    
    bool taxable2 = !((*pi2).isTaxable());

     이런 암시적 변환을 하면 get을 쓰지 않아도 되지만 실수할 여지가 늘어나기도 합니다. 그래서 원치 않은 타입 변환을 제공하기 싫다면 명시적 변환함수만 제공하는게 좋습니다.

     

     추가적으로 스마트 포인터가 캡슐화에 위배가 되는가를 고민할 수 있지만 본 목적이 은닉이 아니라 자원 해제를 편하게 하는 것이기 때문에 캡슐화가 필수는 아닙니다. 그리고 shared_ptr 같은 애들은 엄격한 캡슐화와 느슨한 캡슐화 동시에 지원하는 것들이 있습니다.

    반응형

    댓글

Designed by Tistory.