ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 2장(1) - 생성자, 소멸자 및 대입 연산자
    쾌락없는 책임 (공부)/Effetive C++ 요약본 2022. 3. 6. 21:50
    반응형
    본 카테고리는 프로텍 미디어의 '이펙티브 C++'을 보고 요약하는 카테고리입니다.

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

     

    항목 5 : C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자

    class Empty{};
    이 클래스가 사실은
    
    class Empty{
    public:
    	Empty(){...} // 생성자
    	Empty(const Empty & rhs){...}  // 복사 생성자
    	~Empty(){...} // 소멸자
    
    	Empty& operator = (const Empty & rhs) {...}  // 복사 대입 연산자
    }

     가장 대표적인 예시가 생성자로 만일 맞는 생성자가 없으면 컴파일러가 자동으로 만들어주게 됩니다. 그리고 자동으로 만드는 함수들은 전부 public 멤버이고 inline입니다.

     이중에서 복사 대입 연산자, 복사 생성자 등이 골치가 되는데 특히 참조자를 데이터 멤버로 가지고 있는 경우, 상수 멤버가 있는 경우 큰 문제가 될 수 있습니다. 그러니 문제가 발생될 것 같으면 복사 대입 연산자, 복사 생성자 등을 직접 구현해두는게 좋습니다!

     

     추가적으로 private로 복사 대입 연산자를 선언한 클래스로부터 상속받는 클래스는 암시적으로 복사 대입 연산자를 가질 수 없습니다.

     

     

    항목 6 : 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금지하자

     만약 객체를 생성할 때 그 객체와 같은 값을 가지는 다른 객체가 있어서는 안 될 때 복사 생성자, 대입 생성자는 큰 걸림돌이 됩니다. 그리고 만일 구현을 하지 않으면 컴파일러가 자동으로 생성해 버리니 여러모로 골칫덩이입니다.

     그래서 이것들을 private로 선언을 하면 외부로부터 호출이 되지 않으니 이것들을 일부 막을 수 있습니다.

     

     문제는 friend 함수와 멤버 함수들이 호출할 수 있다는 점입니다. 그래서 여기서 나오는 아이디어가 '선언'을 한 뒤 '구현'을 안 하는 기법입니다. (iostream 에서도 사용하는 방식입니다.)

    class A{
    private:
    	A(const A&);
    	A & operator = (const A&);
    };

     이러면 사용자가 복사를 할 수도 없고(private라 컴파일러에서 태클) friend 함수에서 사용하게 되면 링커에서 태클을 걸게 될 것입니다.

     

     

    항목 7 : 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

     객체를 찍어내 객체 포인터만 반환하는 팩토리 함수도 있을 때 여기서 파생 클래스를 만들어 낼 수도 있습니다. 그래서 다형성을 가지는 클래스라면 가상 소멸자를 기본 클래스에 넣어두는 게 좋습니다.

    // 책에 나온 가상 소멸자를 가진 클래스
    class TimeKeeper{
    public:
    	TimerKeeper();
    	virtual ~TimeKeeper();
    	...
    };
    
    TimeKeeper *ptk = getTimeKeeper();
    
    ...
    delete ptk;  // 이제 제대로 동작

     가상 함수가 있으면 파생 클래스에서 이를 구현해야지 동작을 할 수 있습니다.

    (이와 반대로 기본 클래스가 아니라면 가상으로 선언하지 않도록 합시다. -> virtual table 이 들어가기 때문)

     

     추가적으로 STL의 컨테이너 타입들 같은 애들을 가지고 파생 클래스를 만들려는 경우가 있습니다. string을 파생하는 클래스 SpecialString이 있다고 하면

    SpecialString *pss = new SpecialString("Doom");
    std::string *ps;
    ...
    ps = pss;
    ...
    delete ps;
    // 정의되지 않은 동작!
    // 실질적으로는 *ps에 있는 SpecialString 부분의 자원 누수
    // 그 이유는 SpecialString의 소멸자가 호출되지 않기 때문

     이런 식으로 문제가 있을 수 있으니 STL 컨테이너에서 파생하는 건 가급적 피하도록 합시다. 얘네는 비가상 소멸자를 가지기 때문이죠.

     

     그리고 경우에 따라서 순수 가상 함수로 두는 게 편합니다. 

    - 순수 가상 함수가 있으면 추상 클래스가 된다.

    - 그런데 마땅히 가상 함수로 둘 게 없다.

    => 그럼 소멸자를 순수 가상 함수로 만들어버리자!

     대신 가상 함수를 만들어 뒀으니 이에 대한 정의를 꼭 해줘야 합니다. 이 부분을 잊는다면 링커 에러가 나오니깐요.

    public AMOV{
    public :
    	virtual ~AMOV() = 0;
    };
    
    ...
    
    AMOV::~AMOV(){}

     

     

    항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

     만약 소멸자에서 예외가 터지면 C++ 이 감당하기 힘들어지고 강제 종료가 될 가능성이 있습니다. 만약 예외를 던질 가능성이 있는 소멸자를 둔다면 소멸자가 예외를 밖으로 나가게 할 것입니다. 

     아래처럼 자원관리 클래스에서 close 함수를 불러 소멸에 대한 처리를 해준다고 합시다.

    class DBConn{
    public:
    	...
    	~DBConn()
    	{
    		db.close();
    	}
    
    private:
    	DBConnection db;
    };

     그런데 저기 close에서 예외가 나오면 앞서 말한 대로 예외가 밖으로 나가게 됩니다. 그리고 이걸 피하는 방법은 2가지입니다.

     

    프로그램을 바로 끝내기
    DBConn::~DBConn()
    {
    	try{
    		db.close();
    	}
    	catch(...){
    		close 호출 실패 로그 작성
    		std::abort();
    	}
    }

     

    close를 호출한 곳에서 예외를 삼키기
    DBConn::~DBConn()
    {
    	try{
    		db.close();
    	}
    	catch(...){
    		close 호출 실패 로그 작성
    	}
    }

     때에 따라서 강제 종료보다 나을 수 있지만 정보가 묻힐 수 있어서 대부분의 경우 좋은 선택이 될 수 없습니다. 하지만 간혹 강제 종료보다 나은 경우가 있을 수 있습니다. 대신 강제 종료를 하지 않았으니 그 후 동작들이 신뢰성 있거 흘러가야 합니다.

     

     일단 보면 위 방법 둘 다 별거 없어 보입니다. close 함수가 예외를 던진다면 이 원인에 대한 조치가 일절 없기 때문입니다. 그래서 만약 클래스 연산 진행 중 오류가 나온 거에 대해 사용자가 반응해야 한다면 보통의 함수에게 연산을 제공하는 방법이 있습니다.

    class DBConn{
    public:
    	...
    	void close(){
    		db.close();
    		closed = true;
    	}
    	
    	~DBConn(){
    		if(!closed)
    		try{
    			db.close();
    		}
    		catch(...){
    			close 호출 실패 로그 // 2가지 방법중 하나
    		}
    }
    
    private:
    	DBConnection db;
    	bool closed;
    };

     위 코드에서는 close 동작을 사용자에게 넘겼습니다. 어떻게 보면 무책임할 수 있지만 사용자가 예외 처리를 할 수 있는 길을 열어둔 것이고 혹시나 실패했다 하더라도 소멸자에서 처리를 하니 예외 처리 권한을 가질 수 있게 해 준 것이죠.

     

     이 항목에서 가장 중요하게 다루는 건 소멸자 밖으로 예외가 나가게 하지만 말자니깐요.

    반응형

    댓글

Designed by Tistory.