Item 9 : Never call virtual functions during construction or destruction
객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 않는다.
다음과 같은 코드를 살펴보자.
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;
...
};
Transaction::Transaction()
{
...
logTransaction();
}
class BuyTransaction: public Transaction{
public:
virtual void logTransaction() const;
...
};
class SellTransaction: public Transaction{
public:
virtual void logTransaction() const;
...
};
그리고 다음 코드가 실행되면 무슨 일이 발생할까?
BuyTransaction b;
생성자가 호출될 때, derived class의 base class의 부분이 먼저 생성된 후에 derived class의 부분이 생성된다.
Transaction 생성자는 마지막에 가상함수 logTransaction을 호출하는데, 이때 BuyTransaction이 아닌 Transaction의 logTransaction이 호출된다.
base class의 생성자가 호출되는 동안, 오브젝트는 base class의 type인 것처럼 동작한다.
base class의 생성자가 동작하는 동안 derived class의 member data는 아직 초기화되지 않았다.
만약, base class의 생성자가 동작하는 동안 virtual function이 derived class의 함수를 호출한다면, 해당하는 멤버 데이터가 아직 초기화되지 않았기 때문에 의도하지 않은 동작이 발생할 수 있다.
base class의 생성자가 동작하는 동안, 오브젝트의 타입은 base class이다.
b가 BuyTransaction이라도, base class의 생성자가 동작하는 동안에는 Transaction으로 동작하는 것이다.
오브젝트는 derived class의 생성자 실행이 시작되어야 derived class 타입으로 동작한다.
소멸자(destruction)도 같은 이유가 적용된다.
일단 derived class의 소멸자가 동작하면, 오브젝트의 derived class의 memeber data는 정의되지 않은 값으로 추정하고, 그 값들이 더 이상 존재하지 않는 것으로 여긴다.
base class의 소멸자에 진입하면 오브젝트는 base class의 오브젝트가 된다.
위의 코드와 같은 경우에 logTransaction은 순수 가상함수이고 derived class에서 logTransaction에 대한 구현이 되어 있지 않다.
따라서 링커는 transaction::logTransaction의 구현부를 찾을 수 없기 때문에 오류가 발생한다.
만약 transaction이 여러개의 생성자를 가진다면, 반복되는 코드는 피하는 것이 좋은 소프트웨어다.
모든 transaction의 생성자가 logTransaction을 포함한다면, private non-virtual initialization함수를 만들어 logTransaction을 포함시킨다.
class Transaction{
public:
Transaction()
{ init(); }
virtual void logTransaction() const = 0;
private:
void init()
{
...
logTransaction();
}
};
앞에서 언급했듯이 logTransaction은 순수 가상 함수이기 때문에 오류가 발생한다.
그러나 logTransaction이 보통의 가상 함수라면, 호출되지만 원하지 않는 버전의 logTransaction이 호출될 것이다.
이 문제에 대한 접근 방법은 다음과 같다.
logTransaction을 non-virtual function으로 만든다.
그리고 필요한 정보는 Transaction의 생성자에게 전달한다.
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;
...
};
Transaction::Transaction()
{
...
logTransaction();
}
class BuyTransaction: public Transaction{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters))
{ ... }
private:
static std::string createLogString(parameters);
};
클래스 구성에 필요한 구성 정보를 base class에 전달한다.
createLogString함수를 static으로 만듦으로써, BuyTransaction 오브젝트의 초기화되지 않은 멤버 데이터를 실수로 참조할 위험을 방지한다.
Item 10 : Have assignment operators return a reference to *this
할당 연산자는 *this의 참조자를 반환하게 한다.
class Widget{
public:
...
Widget& operator=(const Widget& rhs) // return type is a reference to the current class
{
...
return *this // return the left-hand object
}
...
};
이것은 단지 관례일 뿐이므로 이를 따르지 않는 코드도 컴파일된다.
그러나 표준 라이브러리에 있는 모든 built-in-type이 규칙을 따르기 때문에 정당한 이유가 없는 한 규칙을 따라야 한다.
Item 11 : Handle assginment to self in operator=
operator=에서는 자기대입에 대한 처리가 빠지지 않도록 한다.
자기대입의 결과로 객체를 참조하는 방법이 두가지 이상이 된다.
일반적으로 동일한 유형의 여러 개체에 대한 참조 또는 포인터에서 작동하는 코드는 개체가 동일할 수 있다는 점을 고려해야 한다.
사실, base class 참조 또는 포인터가 derived class의 오브젝트를 참조하거나 가리킬 수 있기 때문에 두 개체가 동일한 계층 구조에 있는 경우 동일한 형식으로 선언될 필요는 없다.
class Base { ... };
class Derived : public Base { ... };
void doSomething(const Base& rb, Derived* pd);
위의 코드에서 rb와 pd는 실제로 같은 오브젝트일 수 있다.
class Widget{
...
private:
Bitmap *pb;
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
이 코드에서 self assignment problem(자기대입문제)이 발생할 수 있다.
*this와 rhs가 같은 오브젝트일 수 있다.
두 오브젝트가 같은 경우, delete pb를 실행하면 현재 오브젝트의 bitmap 뿐만 아니라 rhs의 bitmap 또한 삭제된다.
이 문제를 예방하는 가장 전통적인 방법은 operator= 가장 위에서 identity test를 하는 것이다.
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this; // identity test
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
그러나 앞에서 언급한 코드는 self-assignment-unsafe일 뿐만 아니라, exception-unsafe하다는 문제점이 있다.
이 버전은 계속해서 예외 문제를 발생시킨다.
new Bitmap 표현식이 예외(할당을 위한 메모리 부족, Bitmap의 복사 생성자가 메모리를 throw한 경우)가 발생하면 Widget은 삭제된 Bitmap에 대한 포인터를 가지게 된다.
그러면 안전하게 delete, read할 수 없다.
따라서, operator=를 self-assignment-safe(자기대입 안전성) 뿐만 아니라 exception-safe(예외 안전성)하도록 해야 한다.
statement의 순서를 조정함으로써 이를 달성할 수 있다.
복사하기 전에 오브젝트를 삭제하면 안된다.
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
위의 코드에서, new Bitmap에서 예외가 발생하면, pb는 변경되지 않는다.
또한, 위 방법은 identity test(일치성 검사)가 필요 없다. 따라서 더 효율적이다.
또 다른 대안으로 copy and swap(복사 후 맞바꾸기)가 있다.
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
정리하자면, operator=가 자기대입하는 경우에도 잘 동작해야 한다. source와 target object의 주소를 비교하는 방법, statement의 순서를 조정하는 방법, 복사 후 맞바꾸는 방법이 있다.
또한, 두 개 이상의 오브젝트에 대해 작동하는 함수가 있는 경우, 파라미터로 넘겨지는 오브젝트가 같은 오브젝트인 경우에 잘 동작하는지 확인해야 한다.
Item 12 : Copy all parts of an object
객체의 모든 부분을 빠짐없이 복사한다.
copy constructor와 copy assignment operator를 copy function이라고 한다.
copy function을 선언할 때 구현하지 않은 부분이 있어도 컴파일러는 잘못된 부분을 알려주지 않는다.
class에 새로운 멤버 데이터를 추가한다면 copy function도 이를 반영해줘야 한다.
클래스를 상속하는 경우 base class의 멤버 데이터도 copy function에 포함해야 한다.
copy constructor의 경우, derived class의 copy constructor만 구현하고 base class의 copy constructor가 없으면, derived class의 멤버 데이터는 복사되고 base class의 멤버 데이터는 복사되지 않는다.
base class는 default constructor를 호출하여 멤버 데이터에 대한 초기화를 수행한다.
copy assignement operator의 경우, copy constructor와 상황이 다르다.
어떤 식으로든 base class의 멤버 데이터 수정을 시도하지 않으면 멤버 데이터가 변경되지 않은 상태로 유지된다.
derived class는 base class의 private 멤버 데이터에 접근할 수 있다.
derived class의 copy function에서 base class의 copy function을 호출함으로써 멤버 데이터를 복사할 수 있다.
즉, derived class의 경우 모든 로컬 멤버 데이터를 복사하고 모든 base class에 대한 적절한 copy function을 호출해야 한다.
복사 기능 중 하나를 다른 기능으로 구현하지 않는다. 대신, 둘 다 호출하는 공통된 기능을 가진 또 다른 함수를 만든다.
두 copy function이 종종 유사한 형태를 가지기 때문에 한 함수가 다른 함수를 호출하도록 하여 코드 중복을 피하려는 시도를 할 수 있다.
그러나 이는 잘못된 방법이다.
예를 들어 copy constructor가 copy assignment operator를 호출하는 경우, copy constructor는 새로운 오브젝트를 초기화 하지만, copy assignment operator의 경우에는 이미 초기화된 오브젝트에 대해 할당을 수행한다.
따라서 초기화되지 않은 오브젝트에 대해 값을 할당하고자 하는 경우 오류가 발생할 수 있다.
대신 두 copy function이 공통적으로 갖고 있는 기능을 가진 새로운 멤버 함수를 만들어 중복을 제거할 수 있다.
이 함수는 일반적으로 private이며 종종 init이라는 이름으로 사용된다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] Chapter 4: Design and Declarations(2) (0) | 2022.07.05 |
---|---|
[Effective C++] Chapter 4: Design and Declarations(1) (0) | 2022.07.02 |
[Effective C++] Chapter 3: Resource Management (0) | 2022.07.01 |
[Effective C++] Chapter 2: Constructors, Destructors, and Assignment Operators(1) (0) | 2022.06.30 |
[Effective C++] Chapter1: Accustoming Yourself to C++ (0) | 2022.06.30 |