Post

[ STUDY/Effective C++ ] 2022. 8. 24. 20:16
x = y = z = 15;

x = (y = ( z = 15);

사슬처럼 엮이는 대입연산의 특징 우측연산을 클래스 대입 연산자에서도 적용을 시키자.

class Widget {
public:
	...
    	Widget& operator=(const Widget& rhs)
    	{
        	...
        	return *this;
    	}
	...
};

좌변 객체를의 참조자를 반환하는 형태는 관례이다. 굳이 안따라도 되지만 모든 기본제공타입들이 따르고 있을뿐만 아니라 표준 라이브러리에 속한 모든 타입들도 따르고 있다.

Post

[ STUDY/Effective C++ ] 2022. 8. 17. 23:06

 

class Transaction {
public:
	Transaction();
	virtual void logTransaction() const = 0;
	...
};

Transaction::Transaction() //기본 클래스 생성자
{
	...
	logTransaction();
}

class BuyTransaction: public Transaction { //Transaction의 파생 클래스
public:
	virtual void logTransaction() const;
	...
};
BuyTransaction b;

위와 같은 코드가 있을때 BuyTransaction b; 코드를 작성한다면 호출되는 logTransaction함수는 BuyTransaction의 것이 아니라 Transaction의 것이다.

 

기본클래스의 생성자가 호출 될 동안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지않는다.

( = 기본 클래스 생성과정에서는 가상함수가 먹히지않는다.)

 

왜냐하면 기본 클래스 생성자는 파생 클래스 생성자보다 앞서 실행되기때문에, 기본 클래스 생성자가 돌아가고 있을 시점에는 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니다.

 

파생클래스(BuyTransaction)만의 데이터가 아직 초기화된 상태가 아니기때문에 아예 없는것으로 취급하는게 안전하다고 생각하기때문에 파생클래스의 객체는 기본 클래스 부분이 생성되는 동안은, 기본 클래스타입의 객체로 취급한다.

 

그리고 C++은 초기화되지 않은 영역을 건드리는 것 자체를 못하도록 막기때문에 가상함수가 파생클래스 쪽으로 내려가지않는다.

 

소멸자 또한 마찬가지로 소멸자가 호출되고나면, C++은 파생 클래스만의 데이터 멤버를 없는것처럼 취급하고 진행한다.

 

 

 

 

이러한 문제점을 해결하기위해서는 어떻게해야할까?

class Transaction {
public:
	explict Transaction(const std::string& logInfo);
	void logTransaction(const std::string& logInfo) const; //비가상 함수
	...
};

Transaction::Transaction(const std::string& logInfo)
{
	...
	logTransaction(logInfo); //비가상 함수 호출
}

class BuyTransaction: public Transaction {
public:
	BuyTransaction(parameters)
	 : Transaction(createLogString (parameters) ) //로그 정보를 기본 클래스 생성자로 넘김
	 {...}
	 ...
     
private:
	static std::string createLogString(parameters);
};

바로 가상함수를 비가상 멤버함수로 바꾸는것이다. 

위의 코드들을 예시로 들자면,

파생클래스의 생성자들이 필요한 로그 정보를 Transaction의 생성자로 넘기면=>

비가상 함수로 바꾼 logTransaction이 안전하게 수행된다. (정적 멤버로도 되어있기때문에 더욱더 안전)

 

필요한 초기화 정보를 파생클래스에서 기본 클래스 생성자로 올려준다는 얘기니

기존에 기본 클래스에서 정의한 가상함수를 파생클래스에서 사용할려고한 의도와 똑같이 사용이 가능하다.

Post

[ STUDY/Effective C++ ] 2022. 8. 17. 19:44

c++은 예외를 내보내는 소멸자를 좋아하지않는다.

당연하다. 소멸자가 그냥 조용히 소멸하는것이 아닌 예외까지 발생하면 프로그램의 불완전 종료 나 미정의 동작의 위험을 내포하고있기때문이다.

 

하지만 항사 우리가 원하는대로 코드를 짤 순 없다.

만약에 예외를 던지고 실패할 수 있는 코드를 소멸자에 넣어한다면 어떻게 해야할까

 

1. 프로그램 바로 끝내기

DBConn::~DBConn()
{
	try {db.close(); }
    catch (...) {
    	close 호출이 실패했다는 로그 출력;
        std::abort();
	}
}

abort를 호출해서 못 볼꼴을 미리 안보여준다.

 

 

2. 예외 삼켜버리기(무시하기)

DBConn::~DBConn()
{
	try {db.close(); }
    catch (...) {
    	close 호출이 실패했다는 로그 출력;
	}
}

발생한 예외를 그냥 무시하더라도 프로그램이 신뢰성 있게 지속될 경우에만 사용하자.

 

 

3. 사용자가 직접처리하기

void close()
{
	db.close();
    closed = true;
}

DBConn::~DBConn()
{
	if(!closed)
	try {db.close(); }
    catch (...) {
    	close 호출이 실패했다는 로그 출력;
        ...//실행끝내거나 예외무시
	}
}

사용자함수를 만들어 DBConnection이 닫혔는지 여부를 유지가능하며 사용자가 에러를 처리할 수 있는 기회를 가질 수 있다. 닫히지않았다면 DBConn의 소멸자에서 닫을 수도 있다. 

 

이렇게하면 데이터베이스 연결이 누출되지않으나, 소멸자에서 호출하는 close마저 실패한다면 다시 위의 1,2번 처럼 돌아올 수 밖에 없긴하다.

 

 

 

Post

[ STUDY/Effective C++ ] 2022. 6. 25. 03:13

다형성을 가진 기본 클래스에서 비가상 소멸자를 통해 소멸하면 어떻게 될까?

class TimeKeeper {
public:
	TimeKeeper();
	~TimeKeeper();
    ...
};
TimeKeeper* getTimeKeeper();
TimeKeeper *ptk = getTimeKeeper();

...
delete ptk;

기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때 비가상 소멸자가 들어 있으면, 대부분 그 객체의 파생 클래스 부분이 소멸되지않게 된다.

파생 클래스의 소멸자도 실행되지 않지만 기본클래스 부분은 소멸 과정이 제대로 끝나 '부분소멸'이 되게된다.

 

이 문제를 없애기위해서는 기본 클래스에 가상 소멸자를 넣어놓으면된다.

class TimeKeeper {
public:
	TimeKeeper();
	virtual ~TimeKeeper();
    ...
};
TimeKeeper* getTimeKeeper();
TimeKeeper *ptk = getTimeKeeper();

...
delete ptk;

소멸자 앞에 virtual 만 붙여줄 뿐인데, 파생 클래스 까지 객체 전부가 소멸된다. 

 

 

 

하지만 기본클래스로 설계되지 않거나 다형성을 갖도록 설계되지않은 클래스조차 마구잡이로 가상소멸자를 선언하면 안된다

class Point {
public:
	Point (int xCoord, int yCoord);
	~Point();
    
private:
	int x,y;
};

위와같은 코드가 있을때, Point객체는 int가 2개 있으니 64bit를 생각하면 64bit레지스터에 맞게된다.

하지만 Point 클래스의 소멸자가 가상 소멸자라면 64bit가 아닌 96bit가 된다.(C++에서는 가상함수를 구현하기 위해 클래스에 별도로 가상 함수 테이블 포인터 라는 자료구조가 들어가야하기 때문이다.)

 

그리고 다른 언어로 선언된 동일한 자료구조와도 호환성을 잃게된다. 다른 언어에선 '가상 함수 테이블 포인터' 라는 vptr이 없기때문이다. 

그렇기때문에 가상 소멸자를 선언하는것은 그 클래스에 가상 함수가 하나라도 있을 경우에만 선언하고, 가상 함수가 없을때에는 가상 소멸자를쓰지말자!

 

 

하지만 모든 기본 클래스가 다형성을 갖도록 설계된것은 아니다.

(표준 string타입, STL컨테이너타입)

그리고 기본클래스로는 쓰일 수 있지만, 다형성은 갖지않도록 설계된것도 있다.

(Uncopyable, 표준 라이브러리의 input_iterator_tag)

이런 클래스들은 파생 클래스 객체의 조작이 허용되지 앟기때문에 가상 소멸자를 볼 수 없다.

 

 

 

+) 순수 가상 소멸자를 이용하면 추상클래스를 쉽게 만들 수 있다.

우리가 아는 추상클래스는 보통 순수가상함수가 들어가 있다.

하지만 추상클래스이면 좋겠지만 넣을 순수 가상 함수가 없다면 굳이굳이 만들어서 넣을필요는 없고,

순수 가상 소멸자만 있더라도 추상클래스가 될 수 있다.

class AWOV {
public:
	virtual ~AWOV() =0; //순수 가상 소멸자 선언
};
AWOV:: ~AWOV() {} //순수 가상 소멸자 정의
▲ top