Item5: C++이 암묵적으로 작성하고 호출하는 함수들 알기
C++컴파일러가 개입하기 때문에 빈 클래스이더라도 빈 클래스가 아니다.
직접 선언하지 않으면, 컴파일러는 복사 생성자, 복사 대입 연산자, 소멸자의 자체 버전을 생성한다.
어떠한 생성자도 선언하지 않는다면 기본 생성자도 자동 생성한다.
이 함수들은 public이며 inline으로 정의된다.
생성된 소멸자는 기본적으로 virtual이 아니다.
복사 생성자, 복사 대입 연산자는 기본적으로 소스 객체의 모든 비정적 데이터 멤버를 단순히 복사하는 방식으로 동작한다.
컴파일러가 자동으로 생성하는 복사 대입 연산자는 생성 시 일반적으로
1. 결과 코드가 올바르게 동작 가능한지(legal) 확인
2. 복사 연산을 수행할 합리적인 방법(resonable change of making sense)이 존재함 확인
을 보고 조건 중 하나라도 충족되지 않는다면 컴파일러는 복사 대입 연산자 생성을 거부한다.
만약의 멤버 변수가 참조타입, 혹은 상수타입이라면 복사 대입 연산자는 자동으로 생성되지 않는다.
또한 base class가 private 복사 대입 연산자를 선언하고 있다면, derived class의 생성 복사 대입 연산자는 이를 호출할 수 없어 거부된다.
Item6: 원하지 않는 컴파일러 자동 생성 함수를 명시적으로 금지하기
함수가 자동으로 동작하는 것을 막는 가장 쉬운 법은 선언하지 않는 것이지만, 자동 생성 함수들이 있기 때문에 다른 조치가 필요하다.
1. 복사 생성자, 복사 대입 연산자를 private으로 선언
2. 정의 없는 선언
C++ 11 이후에는 =delete가 있기 때문에 이를 사용하면 될 것으로 보임
Item7: 다형성 기반 클래스에는 소멸자를 가상 함수로 선언하기
base(기반) class와 derived(파생) class가 있을 때 base class 포인터를 factory function을 사용해 반환받아 사용한다면,
(TimeKeeper 예시)
힙에 객체를 할당하므로 메모리 누수를 막기 위해 적절한 메모리 해제가 필요하다.
만약 base class의 소멸자가 가상(virtual)이 아니라면, derived class의 소멸자는 호출되지 않는다.
결과적으로 리소스 누수, 데이터 손상이 발생할 가능성이 매우 크다.
이를 해결하기 위해 base class의 dtor을 virtual로 선언한다.
가상 소멸자가 필요한 경우와 필요하지 않은 경우
필요: base class가 다형적(polymorphic)인 경우 = 가상 함수가 포함된 경우에는 반드시 소멸자를 virtual로 선언해야 한다.
필요x: 다형성을 가지지 않는 경우, 가상 함수가 없는 경우 해당 클래스는 base class로 사용되지 않을 가능성이 높기때문에 필요x
가상 소멸자에는 성능 오버헤드가 있다. virtual table pointer = vptr로 인해 객체의 크기가 증가한다.
비가상 소멸자가 문제를 일으킬 수 있는 경우
가상 함수가 없는 경우에도 비가상 소멸자가 문제를 일으킬 수 있다.
(책의 예시) string같은 표준 라이브러리의 class는 가상 함수를 포함하지 않으며 소멸자도 가상이 아니다.
하지만 일부 프로그래머들이 이를 base class로 사용하려고 할 수도 있다.
SpecialString* pss = new SpecialString("Impending Doom"); // SpecialString은 string을 base class로 사용중
std::string* ps;
...
ps = pss; // SpecialString* → std::string* 변환
...
delete ps; // 정의되지 않은 동작 발생!
delete ps 실행 시 string의 소멸자만 호출되고, SpecialString의 소멸자는 호출되지 않아 메모리 누수가 발생한다.
STL 컨테이너도 마찬가지이다.
이를 막기 위해 final 키워드를 사용하면 된다.(C++11이상)
순수 가상 소멸자(Pure Virtual Destructor)
클래스를 추상 클래스로 만들고 싶지만, 다른 순수 가상 함수가 없는 경우
소멸자를 순수 가상함수로 선언해 만든다.
이 때 정의가 필요하다.
Item8: 소멸자에서 예외가 나오지 않도록 하기
void doSomething() {
std::vector<Widget> v;
...
} // v는 여기서 자동으로 소멸됨
위와 같은 경우 vector v의 소멸 과정에서 vector내부의 모든 객체를 순차적으로 소멸해야 한다.
만약 소멸 중간에 예외(exception)가 발생하더라도 나머지 객체들은 반드시 소멸되어야 할 것이다.
그런데 소멸자에서 생긴 예외를 처리 중에 다른 객체에서 소멸자가 또 다른 예외를 던진다면 C++은 동시에 두 개의 예외를 처리할 수 없기 때문에 프로그램이 비정상 종료되거나, UB가 발생할 것이다.
이는 모든 컨테이너에서 발생가능하다.
따라서 결론적으로 C++은 소멸자에서 예외를 던지는 것을 허용하지 않는다는 것이다.
소멸자가 반드시 예외를 던질 가능성이 있는 작업을 수행한다면?
class DBConn {
public:
...
~DBConn() {
db.close(); // 데이터베이스 연결을 자동으로 닫음
}
private:
DBConnection db;
};
DB 사용자(client)가 connection을 닫는 것을 까먹지 않게 하기 위해 소멸자에 close를 넣어준 설계이다.
하지만 만약 close()중간에 예외가 발생한다면, 위에 말한 것처럼 terminate 되거나 undefined behavior가 발생할 수 있다.
소멸자에서 예외가 발생하는 문제를 피하는 두 가지 방법(아님)
1. 프로그램 강제 종료 (std::abort() 사용)
DBConn::~DBConn()
{
try {
db.close();
}
catch (...) {
// 로그를 남기고 프로그램 종료
make log entry that the call to close failed;
std::abort();
}
}
장점
- 예외가 전파되기 때문에 UB발생을 방지한다.
- 예외가 치명적인 오류인 경우 적절하다.
단점
- 프로그램이 즉시 종료되어 오류 복구, 데이터 저장 기회가 없다.
- 예외 발생 시 클라이언트가 대응할 방법이 없다.
2. 예외 무시
DBConn::~DBConn()
{
try {
db.close();
}
catch (...) {
// 로그만 남기고 예외를 무시
make log entry that the call to close failed;
}
}
장점
- 특정 상황에서는 프로그램이 계속 실행되는 것이 중요한 경우 유용할 수 있다.
단점
- 실제 문제를 숨길 가능성이 크다.
- 예외가 발생했다는 사실을 프로그램에서 감지가 어려움
- 오류의 원인 추적이 어려움
결론적으로 이 두 가지 방법은 아쉽고, 다른 방법이 필요하다.
소멸자가 아닌 다른 메서드에서 예외 처리
class DBConn {
public:
...
void close() { // 사용자가 직접 호출할 수 있는 함수
db.close();
closed = true;
}
~DBConn() {
if (!closed) { // 사용자가 `close()`를 호출하지 않았다면
try {
db.close(); // 소멸자가 대신 닫기 시도
}
catch (...) {
// 예외 발생 시 로그를 남기고, 프로그램을 종료하거나 삼킴
make log entry that call to close failed;
...
}
}
}
private:
DBConnection db;
bool closed = false; // 연결이 닫혔는지 추적
};
1. 사용자가 예외 직접 처리가능
2. 소멸자에서 예외가 발생할 가능성을 줄임
3. 자동 정리 기능도 유지
현대적인 C++에서는 std::unique_ptr을 사용해 리소스 정리하는 것이 나을지도
'C, C++ > Effective C++' 카테고리의 다른 글
| 4장 설계와 선언 (0) | 2025.03.21 |
|---|---|
| 3장 리소스 관리 (0) | 2025.03.21 |
| 2장 생성자, 소멸자 그리고 할당 연산자 (2) (0) | 2025.03.17 |
| 1장 C++과 친해지기 (2) (0) | 2025.03.15 |
| 1장 C++과 친해지기 (1) (0) | 2025.03.14 |