비공개2011. 7. 7. 16:28

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

C++의 어떤 맴버 함수는 우리가 클래스 안에 직접 선언해 넣지 않으면 컴파일러가 저절로 선언해 주도록 되어 있습니다.
이때 컴파일러가 만드는 함수( 복사 생성자 / 복사 대입연산자 / 소멸자 )는 모두 기본형입니다.

 

 

1
class Empty{};

 

 

1
2
3
4
5
6
7
8
9
10
class Empty
{
public :
        Empty() { }
        Empty( const Empty& ) {       }
        ~Empty() {}
 
        Empty& operator=( const Empty& ) { }
 
};


는 서로 같습니다.

 

기본코드가 만들어지는 조건을 만족하는 코드!!!

 

Empty e1;       // 기본 생성자, 소멸자 

Empty e2(e1); // 복사 생성자 

e2 = e1;         // 복사 대입 연산자

 

 

 

기본 생성자

 

생성자가 존재한다면 기본 생성자는 컴파일러가 자동으로 만들어내지 않는다.

 

 

 

 복사 생성자 / 복사 대입 연산자

 

1) 컴파일러가 자동적으로 생성해주는 복사 대입연산자의 기능은 그저 비정적 데이터를 복사하는 일이다.

 

2) 컴파일러는 컴파일도중 일정한 조건이 성립되어야만 복사 대입 생성자를 만들어준다.

     만약, 맴버 중에 참조자가 있는 경우 복사 대입 연산자는 사용자가 정의 해줘야 한다. 

 

3) 복사 대입 연산자를 private로 선언한 기본 클래스로부터 파생된 클래스의 경우,

    이 클래스는 암시적 복사 대입 연산자를 가질 수 없다.




소멸자

 

소멸자는 이 클래스(빈 클래스)가 상속한 기본 클래스의 소멸자가 가상 소멸자로 되어 있지 않으면
역시 비가상 소멸자로 만들어진다.








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

 

컴파일러에서 자동으로 제공하는 기능을 허용하지 않으려면... 해결법은???

 

해결법 1 ) private으로 복사 생성자, 복사 대입 연산자를 선언만 하기

 

1
2
3
4
5
6
7
8
9
class HomeForSale
{
public :
             ...
 
private :
             HomeForSale( const HomeForSale& ); // 선언만 존재
             HomeForSale& operator=( const HomeForSale& );
};

 

- 다른 부분에서 해당 함수를 호출하려 할 경우 Linke Error!!!

 

해결법 2 ) Base클래스에서 선언하고 그것을 파생클래스에서 상속받게 하기

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UnCopyAble
{
protected :			// 파생된 객체에 대하여
	UnCopyAble()  {}		// 생성과 소멸을
	~UnCopyAble() {}		// 허용합니다.

private :			// 하지만 복사는 방지합니다.
	UnCopyAble( const UnCopyAble& ); 
	UnCopyAble& operator=( const UnCopyAble& );

};

class HomeForSale : private UnCopyAble
{
	...
};

 

- Link Error Compile Error로 바꿈

 

- 참고로, Boost 라이브러리의 noncopyable 클래스는 위의 내용(UnCopyAble)의 기능을 제공한다.










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

 

연관된 객체들 간의 상속된 관계속에서 객체 소멸시 소멸자가 가상소멸자가 아닌 경우

소멸 순서에 의한 문제가 발생할 수 있다.

 

 어떤 경우에 가상 소멸자를 써야 할 것인가?

다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다.
어떤 클래스가 가상 함수를 하나라도 갖고 있으면이 클래스의 소멸자도 가상 소멸자 이어야 합니다.
 

반대로기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는
가상 소멸자를 선언하지 말아야 합니다.

 

■ 무작정 가상 소멸자를 쓰면 안되는 이유

가삼 함수 테이블 포인터라는 별도의 자료구조가 포함되어 객체 사이즈가 커지게 된다.

때문에, 프로그램 실행환경 즉, 32/64비트 아키텍처 중 상황에 따라 퍼포먼스 / 이식성 등의 영향을 미치게 된다.

 

 가상 소멸자가 없는 STL 컨테이너
 
 

Vector , list, set, tr1::unordered_map

 

 Tip : 어떤 클래스가 추상 클래스였으면 좋겠는데 마땅히 넣을 만한 순수 가상 함수가 없을 때

 

1

2

3

4

5

class AWOV

{

public :

               virtual ~AWOV() = 0;

};









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

  

소멸자 내에서 예외가 발생하면 그 후부터 프로그램이 정상적으로 작동하리라는 보장을 잃게 된다.

 
1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
public :
	...
	~Widget(){ ... } // 소멸자에서 예외가 발생한다고 가정합니다.
};

void doSomething()
{
	std::vector<Widget> v;
	...
} // v는 여기서 자동으로 소멸합니다.

 

- 만약 v 10개의 Widget 객체 정보를 담고 있다고 가정하자.

v는 소멸 시점에서 순차적으로 소멸자를 호출 할 것이다.

이때 객체 소멸자를 호출하다가 예외가 발생한다면?

 

 소멸자 내에서 예외 발생

1) 무시        (X)     -> 문제 발생 여지 생김

2) 사용자 처리 (O)

 

 예외 발생 처리 기본  방법  제안

  1) 프로그램을 바로 끝내버립니다.

2) 예외를 삼켜 버립니다.

 

 좀 더 나은 방법   

발생 할 소지가 있는 문제에 대처할 기회를 사용자에게 제공

Point > 예외가 발생한다면 소멸자가 아닌 함수에서 발생하도록 해야 한다.

 

■ 책 요약 

소멸자에서는 예외가 빠져나가면 안 됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야합니다.

어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(, 소멸자가 아닌 함수) 이어야 합니다






 

항목 9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

  
■  생성자에서 가상함수를 호출하는 경우 하나!
 

class Transaction
{
public :
	Transaction() { Init(); }

	virtual void logTransaction() const = 0; // 순수가상함수

	...

private :
	void Init()
	{
		...
		logTransaction(); // virtual 함수 호출???
	}
};

class BuyTransaction : public Transaction
{
public :
	virtual void logTransaction() const ;

	...
};


- 생성자에서 초기화를 할때 아무함수나 신경안쓰고 호출해도 되겠지? 아래 사항을 고려해보자.

1) BuyTransaction 객체가 생성 될 때 Base 클래스인 Transaction 생성자가 먼저 호출 된다.

2) Base 생성자 호출 시점에서는 파생 클래스들의 정보는 초기화가 안되어있다. ( 미정의 )

3) 파생 클래스의 기본 클래스가 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스이다.
    호출되는 모든 가상 함수는 Base 클래스( Transaction )의 것으로 결정된다.

    즉, 이때 순수가상함수를 호출하게 된다. -> 프로그램 종료

4) 컴파일시 에러조차 발생하지 않는다.
   ( Transaction 생성자 호출시!! Init() 아닌 직접 virtual void 
logTransaction() 함수를 사용하면 Link 에러 발생 )



■ 해결법 하나!

 
 

class Transaction
{
public :
	explicit Transaction( const std::string& logInfo );

virtual void logTransaction( const std::string& logInfo ) const; // 비가상 함수 ... }; Transaction::Transaction( const std::string& logInfo ) { ... logTransaction(logInfo); // 비가상함수 호출 } class BuyTransaction : public Transaction { public : BuyTransaction( param ) :Transaction( createLogString( param ) ) // 로그 정보를 기본 클래스 생성자로 넘김 { ... } ... private : static std::string createLogString( param ); // 도우미 함수 };


1) 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 '올려'주도록 하자( 도우미 함수 활용 )

2) 기본 생성자에서 절대로! 가상 함수 호출은 하지 않도록하자. 다른 방법 강구!!



■ 책 요약

생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요.
가상 함수라고 해도, 지금 실행중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않으니까요.



 


 
 

항목 10. 대입 연산자는 *this의 참조자를 반환하게 하자

 

1
2
3
4
5
6
7
8
9
10
class Widget
{
public :
	...
	Widget& operator=( const Widget& rhs ) // +=, -=, *= 에도 동일한 규약이 적용
	{
	        ...
	      return *this;
	}
};



1
2
3
Widget a,b,c;

a = b = c; // 이런 연산이 가능케 한다.


■ 이것은 관례이다.

모든 기본제공 타입들이 따르고 있을 뿐만 아니라 표준 라이브러리에 속한 모든 타입에서도 따르고 있다.

고로, 나도 따르자. ㅎㅎ









항목 11. operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자   


코딩을 하다보면 자기대입의 경우가 발생할 수 있다.
이때 문제가 발생할 수도 있는데 다음과 같은 경우이다.


1
2
3
4
5
6
7
8
// 안전하지 않게 구현된 코드
Widget& Widget::operator=( const Widget& rhs )
{
	delete pBmp;			// 현재의 비트맵 사용을 중지합니다.
	pBmp = new Bitmap(*rhs.pBmp);	// 이제 rhs의 비트맵을 사용하도록 만듭니다.

	return *this;
}


만약 위 코드에서 삭제되는 pBmp 와 인자로 받는 rhs,pBmp가 같은 객체라면?
4번 라인에서 이미 pBmp는 삭제된다. 
5번 라인에서 인자로 받는 정보를 가지고 새로운 Bitmap을 생성해야한다라면
      pBmp는 결국 어떻게 될 것인가???
      다는 몰라도 분명한건 시스템을 불안정한 상태로 만드는 것임에는 틀림없다.

책에서는 자기대입의 위험성에 대한 몇가지 대안을 제시한다. 

1) 일치성 검사

1
2
3
4
5
6
7
8
9
Widget& Widget::operator=( const Widget& rhs )
{
	if( this == &rhs ) return *this; // 객체가 같은지 검사하여 일치할 경우 아무일도 안한다.

	delete pBmp;					
	pBmp = new Bitmap(*rhs.pBmp);	

	return *this;
}


2) 예외 안정성 보완

1
2
3
4
5
6
7
8
9
Widget& Widget::operator=( const Widget& rhs )
{
	Bitmap *pOrig = pBmp;	    // pBmp를 가르키는 포인터를 준비합니다.

	pBmp = new Bitmap( *rhs.pBmp ); // pBmp는 새로운 객체를 가리킵니다.
	delete pOrig;		    // 원래 pBmp가 가리키는 객체를 삭제합니다.

	return *this;
}

이 방법은 Bitmap을 동적할당하여 생성할 때 문제가 발생했다하더라도, 원본 Bitmap 데이터는 보존이 된다고 책은 말합니다.

허나, 제가 알기론 동적할당시 문제가 발생하여 예외가 발생하였을 경우! 프로그램이 죽거나, NULL을 반환하고
계속 진행하는 것으로 알고 있습니다. 결국 6번라인은 실행이 되는 셈이지요. 그렇다면 데이터는 보존이 되지 않습니다.
뭔가 심도깊고 유식해보이는 방법일지언정 현업에서는 1) 일치성 검사 수준의 대응이 적절하지 않을까 생각해봅니다.

3) Copy and Swap

1
2
3
4
5
6
7
8
Widget& Widget::operator=( const Widget& rhs )
{
	Widget temp( rhs ); // rhs의 사본 하나 생성

	swap(temp);         // *this 의 데이터를 위의 사본과 맞바꿈

	return *this;
} 

임시 객체를 생성한 후 객체의 내용을 맞바꾸는 방식




■ 책 요약

operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만듭시다. 

원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 
복사 후 맞바꾸기 기법을 써도 됩니다. 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 
이 함수에 넘겨지는 객체들이 사실같은 객체인 경우에 정확하게 동작하는지 확인해 보세요.  









항목
 12. 객체의 모든 부분을 빠짐없이 복사하자


class User : public CObject // 상속이 안되있다고 가정!
{
public :

	// 기본 생성자
	explicit User()
		: name( "None" )
	{
		/* Nothing */
	}

	explicit User( const std::string& _data )
		: name( _data )
	{

	}

	// 소멸자
	virtual ~User() { }

	// 복사 생성자
	explicit User( const User& _data )
		: name( _data.name )
	{
		/* Nothing */
	}

	// 복사 대입 연산자 
	const User& operator=( const User& _data )
	{
		name = _data.name;

		return *this;
	}

private :
	std::string name;
	CObject     m_Obj; // 맴버가 아니라고 가정!
	
};


이처럼 최초 User 클래스를 이쁘게 만들어놨다 치자!
추후에 다음 클래스가 맴버 or 상속으로 쓰인다면?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CObject
{
public :
	explicit CObject()
		: nObjectID(0),
		    nRenderType(0)
	{ /* Nothing */ }

	virtual ~CObject() { /* Nothing */ }

public :
	int nObjectID;
	int nRenderType;
};
 


처음 코드에서 맴버 or 상속이 안되있다고 한 가정들을 하나씩 풀어서 생각해보자.


1
2
3
4
5
6
7
8
9
10
11
12
void main()
{
        User pc1;

 	 pc1.nObjectID   = 999;
 	 pc1.nRenderType = 1;

	User pc2(pc1); // 복사 생성자 호출! CObject 값이 복사 되지 않음

	User pc3;
	pc3 = pc1;      // 복사 대입 연산자 호출! CObject 값이 복사 되지 않음	
}
 

맴버 or 상속 어떤 경우라도 애초에 의도한 대로 모든 데이터가 복사 되지 않는다. 


편리한 해결법은 없다. 경우에 맞게 빠뜨리지 말고 '복사 생성자' 와 '복사 대입 연산자'에 추가된 것들 모두 추가해주자.

클래스를 설계할 때 초기화 및 복사 해주는 함수를 두면 신경을 좀 덜쓰게 해줄 수 있다.
파생 클래스 이든 가져다 쓰는 클래스이든 사용되어지는 클래스에 초기화 및 복사관련 도우미 함수가 있다면
가져다 쓰는 사용자는 아! 초기화 or 복사 함수를 수정해야겠구나! 라고 생각나게 해주는 정도??? ㅋㅋㅋ
직접 추가해야 되는건 매한가지이다 ㅋ



■ 책 요약

객체 복사 함수는 주어진 객체의 모든 데이터 맴버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 합니다.

클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 마세요.
그 대신, 공통된 동작을 제 3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결합시다.
  

 

Posted by 닭꽝