Item5 : Know what functions C++ silently writes and calls
copy constructor, copy assignment, destructor, 그리고 default constructor는 프로그래머가 선언하지 않으면 컴파일러가 선언한다. 이 모든 함수들은 public이고 inline이다.
*inline: 호출될 때 일반적인 함수의 호출과정을 거치지 않고 함수의 모든 코드를 호출된 장소에 삽입하는 함수
이 함수들은 필요할 때 생성된다.
default constructor와 destructor는 주로 기본 클래스의 생성자, 소멸자 호출과 같은 코드를 컴파일러에 넣을 수 있는 공간을 제공한다. 이 때 생성된 소멸자는 virtual 멤버 함수가 아니다.
컴파일러가 생성한 copy constructor와 copy assignment operator는 단순히 static이 아닌 source object의 모든 멤버 데이터를 target object로 복사한다.
만약, 클래스의 멤버 데이터로 reference나 const 타입의 변수가 있는 경우, 컴파일러가 자동으로 생성한 copy assginment operator를 사용해 값을 할당하고자 할 때, C++는 컴파일을 거부한다.
reference 타입의 변수를 할당하고자 하는 경우, 직접 copy assignment operator를 정의해야 한다.
Item6 : Explicitly disallow the use of compiler-generated functions you do not want
컴파일러가 만들어낸 함수가 필요없는 경우 금지한다.
만약, 프로그래머가 클래스의 특정 함수의 기능을 지원하고 싶지 않다면, 그것을 제공하지 않는 함수를 선언하지 않으면 된다.
그러나, copy constructor와 copy assignment operator의 경우 이 방법은 효과가 없다.
Item5에서 명시한 것처럼, copy constructor와 copy assignment operator는 선언하지 않으면 컴파일러에 의해 자동으로 생성된다.
대안으로 copy constructor와 copy assignment operator를 private으로 선언하는 방법이 있다.
명시적으로 멤버 함수를 private으로 선언함으로써 컴파일러가 자동으로 생성하는 것을 막을 수 있다.
멤버 함수를 private으로 선언하고 정의하지 않는 것, 즉 함수를 구현하지 않는 이 트릭은 잘 설정된다.
몇몇 클래스들에서 복사를 막을 수 있다.
그러나, 이 방법은 error를 발생시킬 수 있다.
멤버 함수가 private으로 선언되었기 때문에 member나 friend function은 여전히 private으로 선언한 함수를 호출할 수 있다. 그러나, 함수가 구현되지 않았기 때문에 호출하면 오류가 발생한다.
복사를 막기 위한 base class를 사용하여 이 문제를 해결할 수 있다.
Uncopyable이라는 base class에 copy constructor와 copy assignment operator를 private으로 선언하고, 다른 클래스가 이를 상속받도록 한다.
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable);
};
class HomeForSale: private Uncopyable{
...
};
이 방법은 잘 동작한다. 누군가가 함수를 호출하면 컴파일러가 copy constructor와 copy assginment operator를 생성한다. 이 컴파일러가 생성한 멤버 함수는 그들의 base class의 함수를 호출한다. 그러나 base class에 private으로 선언되어 있기 때문에 이 호출은 거부된다.
Item7 : Declare destructors virtual in polymorphic base classes
다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언해야 한다.
다음과 같은 코드를 살펴보자.
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper{ ... };
class WaterClock: public TimeKeeper{ ... };
class WritstWatch: public TimeKeeper{ ... };
그리고 derived class의 오브젝트를 새롭게 생성해서 base class의 pointer로 리턴하는 함수 getTimeKeeper가 있다.
getTimeKeeper가 반환하는 값은 heap에 위치하기 때문에, 메모리 누수문제 등의 문제를 방지하기 위해 오브젝트가 적절하게 delete되어야 한다.
TimeKeeper* getTimeKeeper();
TimeKeeper* ptk = getTimeKeeper();
delete ptk;
문제는 getTimeKeeper함수가 derived class의 object에 대한 포인터를 반환하고, 이 오브젝트는 base class의 포인터를 통해 삭제된다는 것이다. 만약, base class가 non-virtual destructor를 가진다면 문제가 발생한다.
오브젝트의 base class부분은 삭제되고, derived class의 부분은 절대로 삭제되지 않는다.
오브젝트가 부분적으로 삭제되는 문제가 발생한다.
이는 메모리 누수, 손상된 데이터 구조, 많은 디버깅 시간을 야기한다.
이 문제를 해결하는 방법은 간단하다.
base class의 destructor를 virtual destructor로 만든다.
그러면, 원하는 대로 derived class의 부분까지 포함하여 오브젝트가 완전히 삭제된다.
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
만약, 클래스가 어떠한 virtual function도 포함하지 않는다면, 이것은 종종 base class로 사용되지 않음을 의미한다.
클래스가 base class가 아닌 경우에 destructor를 virtual로 만드는 것은 나쁜 아이디어다.
virtual function을 구현하는 것은 오브젝트가 추가적인 정보를 필요로 하기 때문에, 오브젝트의 사이즈도 증가한다.
오브젝트는 추가적으로 virtual table pointer(vptr)와 virtual table(vtbl)이 필요하다.
다른 언어를 통해 구현한 것과 더이상 같은 구조를 가지지 않게 된다.
모든 base class가 다형성을 가지도록 디자인된 것은 아니다. string이나 STL container가 그 예다.
다형성을 가지지 않는 base class는 virtual destructor가 필요 없다.
Item8 : Prevent exceptions from leaving destructors
예외가 소멸자를 떠나지 못하도록 붙잡아 두자.
C++는 destructor에서 예외가 발생하는 것을 금지하지는 않지만, 이는 문제가 된다. 아래 코드를 살펴보자.
class Widget{
public:
...
~Widget(){ ... }
};
void doSomeThing()
{
std::vector<Widget>v;
} // v is automatically destroyed here
vector v가 삭제되면 Widget도 함께 삭제되어야 한다.
이때 첫 번째 요소의 destruction에서 예외가 발생 했다고 가정하자.
예외가 발생했더라도 나머지 Widget을 삭제해야 하므로 다른 모든 Widget에 대해 소멸자를 호출해야 한다.
그러나 이러한 호출 중에 두 번재 Widget의 소멸자가 예외를 발생시킨다고 가정하자.
이 경우, C++에는 두 개의 동시 활성 예외가 존재한다.
동시 활성 예외 쌍이 발생하면 프로그램은 정의 되지 않은 동작을 종료하거나 생성한다.
프로그램 조기 종료 또는 정의되지 않은 동작은 컨테이너나 array를 사용하지 않을 때도 예외를 발생시키는 소멸자로 인해 발생할 수 있다.
C++는 예외를 발생시키는 소멸자를 좋아하지 않는다.
trouble을 막는 두 가지 방법이 있다.
1. 프로그램 종료
이 옵션은 삭제 중 오류가 발생한 후 프로그램을 계속 실행할 수 없는 경우에 적합하다.
정의되지 않은 동작을 방지할 수 있다는 장점이 있다.
2. 예외 무시하기
일반적으로 예외를 무시하는 것은 좋지 않은 방법이다.
어떤 동작이 실패했다는 중요한 정보를 억제한다.
그러나 때때로 예외를 무시하는 것이 프로그램 종료 또는 정의되지 않은 동작의 위험을 실행하는 것보다 더 바람직하다.
이것이 가능하려면, 오류가 발생하고 무시된 후에도 프로그램이 안정적으로 실행될 수 있어야 한다.
가장 좋은 전략은 발생할지도 모르는 문제에 대응할 기회를 주는 것이다.
만약 소멸자에서 호출된 함수가 throw된다면, 소멸자는 어떤 예외라도 catch해야 한다.
그런 다음, 예외를 무시하거나 프로그램을 종료한다.
~DBConn()
{
if(!closed)
{
try{
db.close();
}
catch(...){
make log entry that call to close failed;
... // terminate or swallow
}
}
}
만약 클래스가 작업 중에 발생하는 예외에 대응할 수 있어야 하는 경우, 클래스는 작업을 수행하는 일반(non-destructor)함수를 제공해야 한다.
'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(2) (0) | 2022.06.30 |
[Effective C++] Chapter1: Accustoming Yourself to C++ (0) | 2022.06.30 |
Item5 : Know what functions C++ silently writes and calls
copy constructor, copy assignment, destructor, 그리고 default constructor는 프로그래머가 선언하지 않으면 컴파일러가 선언한다. 이 모든 함수들은 public이고 inline이다.
*inline: 호출될 때 일반적인 함수의 호출과정을 거치지 않고 함수의 모든 코드를 호출된 장소에 삽입하는 함수
이 함수들은 필요할 때 생성된다.
default constructor와 destructor는 주로 기본 클래스의 생성자, 소멸자 호출과 같은 코드를 컴파일러에 넣을 수 있는 공간을 제공한다. 이 때 생성된 소멸자는 virtual 멤버 함수가 아니다.
컴파일러가 생성한 copy constructor와 copy assignment operator는 단순히 static이 아닌 source object의 모든 멤버 데이터를 target object로 복사한다.
만약, 클래스의 멤버 데이터로 reference나 const 타입의 변수가 있는 경우, 컴파일러가 자동으로 생성한 copy assginment operator를 사용해 값을 할당하고자 할 때, C++는 컴파일을 거부한다.
reference 타입의 변수를 할당하고자 하는 경우, 직접 copy assignment operator를 정의해야 한다.
Item6 : Explicitly disallow the use of compiler-generated functions you do not want
컴파일러가 만들어낸 함수가 필요없는 경우 금지한다.
만약, 프로그래머가 클래스의 특정 함수의 기능을 지원하고 싶지 않다면, 그것을 제공하지 않는 함수를 선언하지 않으면 된다.
그러나, copy constructor와 copy assignment operator의 경우 이 방법은 효과가 없다.
Item5에서 명시한 것처럼, copy constructor와 copy assignment operator는 선언하지 않으면 컴파일러에 의해 자동으로 생성된다.
대안으로 copy constructor와 copy assignment operator를 private으로 선언하는 방법이 있다.
명시적으로 멤버 함수를 private으로 선언함으로써 컴파일러가 자동으로 생성하는 것을 막을 수 있다.
멤버 함수를 private으로 선언하고 정의하지 않는 것, 즉 함수를 구현하지 않는 이 트릭은 잘 설정된다.
몇몇 클래스들에서 복사를 막을 수 있다.
그러나, 이 방법은 error를 발생시킬 수 있다.
멤버 함수가 private으로 선언되었기 때문에 member나 friend function은 여전히 private으로 선언한 함수를 호출할 수 있다. 그러나, 함수가 구현되지 않았기 때문에 호출하면 오류가 발생한다.
복사를 막기 위한 base class를 사용하여 이 문제를 해결할 수 있다.
Uncopyable이라는 base class에 copy constructor와 copy assignment operator를 private으로 선언하고, 다른 클래스가 이를 상속받도록 한다.
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable);
};
class HomeForSale: private Uncopyable{
...
};
이 방법은 잘 동작한다. 누군가가 함수를 호출하면 컴파일러가 copy constructor와 copy assginment operator를 생성한다. 이 컴파일러가 생성한 멤버 함수는 그들의 base class의 함수를 호출한다. 그러나 base class에 private으로 선언되어 있기 때문에 이 호출은 거부된다.
Item7 : Declare destructors virtual in polymorphic base classes
다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언해야 한다.
다음과 같은 코드를 살펴보자.
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper{ ... };
class WaterClock: public TimeKeeper{ ... };
class WritstWatch: public TimeKeeper{ ... };
그리고 derived class의 오브젝트를 새롭게 생성해서 base class의 pointer로 리턴하는 함수 getTimeKeeper가 있다.
getTimeKeeper가 반환하는 값은 heap에 위치하기 때문에, 메모리 누수문제 등의 문제를 방지하기 위해 오브젝트가 적절하게 delete되어야 한다.
TimeKeeper* getTimeKeeper();
TimeKeeper* ptk = getTimeKeeper();
delete ptk;
문제는 getTimeKeeper함수가 derived class의 object에 대한 포인터를 반환하고, 이 오브젝트는 base class의 포인터를 통해 삭제된다는 것이다. 만약, base class가 non-virtual destructor를 가진다면 문제가 발생한다.
오브젝트의 base class부분은 삭제되고, derived class의 부분은 절대로 삭제되지 않는다.
오브젝트가 부분적으로 삭제되는 문제가 발생한다.
이는 메모리 누수, 손상된 데이터 구조, 많은 디버깅 시간을 야기한다.
이 문제를 해결하는 방법은 간단하다.
base class의 destructor를 virtual destructor로 만든다.
그러면, 원하는 대로 derived class의 부분까지 포함하여 오브젝트가 완전히 삭제된다.
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
만약, 클래스가 어떠한 virtual function도 포함하지 않는다면, 이것은 종종 base class로 사용되지 않음을 의미한다.
클래스가 base class가 아닌 경우에 destructor를 virtual로 만드는 것은 나쁜 아이디어다.
virtual function을 구현하는 것은 오브젝트가 추가적인 정보를 필요로 하기 때문에, 오브젝트의 사이즈도 증가한다.
오브젝트는 추가적으로 virtual table pointer(vptr)와 virtual table(vtbl)이 필요하다.
다른 언어를 통해 구현한 것과 더이상 같은 구조를 가지지 않게 된다.
모든 base class가 다형성을 가지도록 디자인된 것은 아니다. string이나 STL container가 그 예다.
다형성을 가지지 않는 base class는 virtual destructor가 필요 없다.
Item8 : Prevent exceptions from leaving destructors
예외가 소멸자를 떠나지 못하도록 붙잡아 두자.
C++는 destructor에서 예외가 발생하는 것을 금지하지는 않지만, 이는 문제가 된다. 아래 코드를 살펴보자.
class Widget{
public:
...
~Widget(){ ... }
};
void doSomeThing()
{
std::vector<Widget>v;
} // v is automatically destroyed here
vector v가 삭제되면 Widget도 함께 삭제되어야 한다.
이때 첫 번째 요소의 destruction에서 예외가 발생 했다고 가정하자.
예외가 발생했더라도 나머지 Widget을 삭제해야 하므로 다른 모든 Widget에 대해 소멸자를 호출해야 한다.
그러나 이러한 호출 중에 두 번재 Widget의 소멸자가 예외를 발생시킨다고 가정하자.
이 경우, C++에는 두 개의 동시 활성 예외가 존재한다.
동시 활성 예외 쌍이 발생하면 프로그램은 정의 되지 않은 동작을 종료하거나 생성한다.
프로그램 조기 종료 또는 정의되지 않은 동작은 컨테이너나 array를 사용하지 않을 때도 예외를 발생시키는 소멸자로 인해 발생할 수 있다.
C++는 예외를 발생시키는 소멸자를 좋아하지 않는다.
trouble을 막는 두 가지 방법이 있다.
1. 프로그램 종료
이 옵션은 삭제 중 오류가 발생한 후 프로그램을 계속 실행할 수 없는 경우에 적합하다.
정의되지 않은 동작을 방지할 수 있다는 장점이 있다.
2. 예외 무시하기
일반적으로 예외를 무시하는 것은 좋지 않은 방법이다.
어떤 동작이 실패했다는 중요한 정보를 억제한다.
그러나 때때로 예외를 무시하는 것이 프로그램 종료 또는 정의되지 않은 동작의 위험을 실행하는 것보다 더 바람직하다.
이것이 가능하려면, 오류가 발생하고 무시된 후에도 프로그램이 안정적으로 실행될 수 있어야 한다.
가장 좋은 전략은 발생할지도 모르는 문제에 대응할 기회를 주는 것이다.
만약 소멸자에서 호출된 함수가 throw된다면, 소멸자는 어떤 예외라도 catch해야 한다.
그런 다음, 예외를 무시하거나 프로그램을 종료한다.
~DBConn()
{
if(!closed)
{
try{
db.close();
}
catch(...){
make log entry that call to close failed;
... // terminate or swallow
}
}
}
만약 클래스가 작업 중에 발생하는 예외에 대응할 수 있어야 하는 경우, 클래스는 작업을 수행하는 일반(non-destructor)함수를 제공해야 한다.
'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(2) (0) | 2022.06.30 |
[Effective C++] Chapter1: Accustoming Yourself to C++ (0) | 2022.06.30 |