readme.md
기록소
readme.md
전체 방문자
오늘
어제
  • 분류 전체보기
    • 네트워크
      • HTTP
      • 윈도우 소켓 프로그래밍
    • Windows API
    • 그래픽스
      • DirectX11
    • 일반
      • Linux
      • 데이터베이스
      • 팁
      • 책 후기
    • 쿠버네티스
    • 프로그래밍 언어
      • C#
      • Java
      • Go
      • C++
      • Lua
    • 책
      • 이펙티브 C++
      • 제프리 리처의 WINDOWS VIA C, C++
    • 기타

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • id3d11shaderresourceview
  • 생성자
  • 템플릿
  • imagestride
  • new
  • Graphics
  • consteval
  • 초기화
  • 설계
  • 윈도우 소켓
  • phong
  • DirectX
  • 자바8
  • C++
  • 대입연산자
  • emplace
  • 캐스팅
  • 자원관리
  • 인터페이스
  • windowsAPI
  • directx11
  • 소멸자
  • CPP
  • 가상함수
  • Delete
  • wm_keyup
  • 상속
  • const
  • 버텍스 버퍼
  • 소켓 프로그래밍

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
readme.md

기록소

책/이펙티브 C++

이펙티브 C++(99p ~ 111p)

2024. 1. 14. 21:22

이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 99p ~ 111p

요약

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

  • 객체 생성 및 소멸 과정 중에는 가상 함수를 호출하면 절대로 안 된다.
    • 해당 과정에서 호출한 가상 함수는 우리가 원하는 대로 돌아가지 않을 것이다.
  • 주식 거래를 위해 다음과 같은 클래스를 정의했다고 가정하자.
// 주식 거래에 대한 기본 클래스
// 주식 거래 모델링에 있어 중요한 기능은 감사(audit) 기능
// 따라서 주식 거래 객체가 생성될 때마다 감사 로그를 기록하기 위한 기능이 필요
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;
  • 마지막 줄의 코드가 실행되면 BuyTransaction의 생성자가 호출되는 것은 어쨌든 맞는 일이다.
    • 그러나 파생 클래스의 객체가 생성될 때 그 객체의 기본 클래스의 생성자가 파생 클래스 부분보다 먼저 호출된다.
    • 이때 기본 클래스인 Transaction의 생성자를 보면 가상 함수인 logTransaction이 호출되고 있는 것을 볼 수 있다.
  • 기본 클래스의 생성자가 호출될 동안에는 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다.
  • 파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스이다.
    • 따라서 이 과정에서 호출되는 가상 함수는 모두 기본 클래스의 것으로 결정될 뿐만 아니라, 런타임 타입 정보를 사용하는 언어 요소(ex. dynamic_cast)를 사용한다고 해도 이 순간에는 모두 기본 클래스 타입의 객체로 취급한다.
    • BuyTransaction 객체의 경우 기본 클래스 부분을 초기화하기 위해 Transaction의 생성자가 실행되는 동안에는, 그 객체의 타입이 Transaction으로 처리되며, 파생 클래스인 BuyTransaction의 데이터가 아직 초기화되지 않은 상태이기 때문에 파생 클래스를 아예 없는 것처럼 취급하는 것이 안전하다고 볼 수 있다.
  • 객체가 소멸될 때도 동일하게 생각하면 된다.
    • 파생 클래스의 소멸자가 일단 호출되고 나면 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정하기 때문에, C++는 파생 클래스를 없는 것처럼 취급하고 소멸자를 실행한다.

생성자 혹은 소멸자에서 가상 함수가 호출되는지를 잡아내는 방법

  • 기본 클래스의 생성자가 여러 개 된다고 가정하면, 각 생성자에서는 분명히 공통적으로 처리하는 부분이 생기게 될 것이다. 이러한 작업을 모아 공용으로 사용하는 초기화 코드를 만들어 두면 코드의 중복을 예방할 수 있을 것이다.
class Transaction {
public:
    // 공용 초기화 함수 init() 호출
    Transaction() { init(); }

    // 주식 거래 타입에 따라 달라지는 로그 기록을 생성
    virtual void logTransaction() const = 0;

private:
    // 비가상 함수에서 가상 함수를 호출
    void init() {
        // 공용 초기화 로직 수행
        logTransaction();
    }
};
  • 이런 코드는 logTransaction이 순수 가상 함수로 선언되어 있는데도 불구하고 컴파일과 링크 단계에서 에러가 발생하지 않기 때문에 더 위험한 코드라고 볼 수 있다.
    • 대부분의 시스템은 순수 가상 함수가 호출될 때 프로그램을 바로 끝내 버린다.(abort)
    • 아니면 logTransaction이 가상 함수(순수 가상 함수 아님)이며, 기본 클래스 Transaction에서 가상 함수를 구현하고 있을 경우에는 더 복잡해진다.
      • 프로그램은 정상적으로 실행되는 것처럼 보이긴 할 것이다.
      • 하지만 파생 클래스의 생성자가 호출될 때, 로그 기록은 항상 Transaction의 타입으로 결정될 것이기 때문에 개발자를 더 피곤하게 만들 가능성이 있다.
  • 따라서 생성자나 소멸자 호출 과정에서 가상 함수가 호출되는지 잡아내는 방법은 프로그래머가 코드를 철저하게 관리하고, 생성자와 소멸자가 호출하는 모든 함수들이 똑같은 제약을 따르도록 만들어야 한다.
    • logTransaction을 Transaction 클래스의 비가상 멤버 함수로 변경하고, 파생 클래스의 생성자들에서는 로깅에 필요한 정보를 Transaction의 생성자로 전달해야 한다는 규칙을 만들어 보자.
// 생성자와 소멸자에서 호출하는 함수들이 가상 함수가 될 수 없도록 만드는 규칙 적용
#include <string>

class Transaction {
public:
    explicit 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(const std::string& logInfo) : Transaction{ logInfo };
    BuyTransaction(parameter) : Transaction(createLogString(parameter));

private:
    static std::string createLogString(parameters);
};
  • 기본 클래스 부분이 생성될 때는 가상 함수를 호출한다 해도 기본 클래스의 울타리를 넘어 내려갈 수 없기 때문에, 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스의 생성자로 올려주도록 만드는 것이다.

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

  • C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있는 성질을 가지고 있다.
int x, y, z;
x = y = z= 15;
  • 대입 연산은 우측 연관 연산이다. 따라서 위의 코드에서 발생하는 대입 연산 사슬은 다음과 같이 분석된다.
x = ( y = ( z = 15));
  • 15가 z에 대입되고, 그 대입 연산의 결과가 y에 대입되고, 그 결과가 x에 대입된다.
  • 이렇게 대입 연산이 사슬처럼 엮이려면 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있을 것이다.
    • 이런 구현은 일종의 컨벤션인데, 개발자가 만드는 클래스에 대입 연산자가 혹시라도 사용된다면 이 컨벤션을 지키는 것이 좋다.
  • 컨벤션은 따르지 않더라도 코드를 작성하는데 문제가 발생하거나 컴파일 과정에서 문제가 발생하지는 않는다.
    • 하지만 이 컨벤션은 모든 기본제공 타입들이 따르고 있고, 표준 라이브러리에서도 따르고 있기 때문에 지키는 것이 좋다.

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

  • 자기대입(self assignment)이란, 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 의미한다.
class Widget { ... };

Widget w;
w = w;
  • 포인터나 레퍼런스가 자기대입을 하게 되면 중복참조 현상이 발생한다.
    • 중복참조: 여러 곳에서 하나의 객체를 참조하는 상태
  • 같은 타입으로 만들어진 객체 여러 개를 레퍼런스, 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적으로 바람직한 자세가 되겠다.
  • 대입 연산자는 자기대입에 대해 안전하게 동작해야 한다.

class Bitmap {};

class Widget {
public:

    // 안전하지 않게 구현된 대입연산자
    // *this와 rhs가 같은 객체일 가능성이 있음
    // 이 경우에 delete pb를 하면 this뿐만 아니라 rhs의 객체에도 적용됨
    Widget& operator=(const Widget* rhs) {
        delete pb;
        pb = new Bitmap(*rhs->pb);

        return *this;
    }

private:
    Bitmap* pb;
};
  • 이러한 문제를 해결하기 위한 전통적인 방법은 대입 연산자의 첫 단락에 일치성 검사를 통해 자기대입을 검사하는 것이다.
    • 이 방법은 예외 안전성에 대해서 문제가 있다.
    • new Bitmap이라는 표현을 사용할 때, 예외가 터지게 되면(동적 할당에 필요한 메모리 부족, Bitmap 클래스 복사 생성자에서 예외 등) Widget 객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가리키게 된다.
    • 이런 포인터는 delete 연산자를 안전하게 적용할 수도 없고, 안전하게 읽는 것도 불가능하다.
// 자기대입 검사
Widget& Widget::operator=(const Widget& rhs) {
    if (this == &rhs) return *this;

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

    return *this;
}
  • 따라서 대입 연산자를 예외에서 안전하게 구현하면 된다.
    • 많은 경우에 문장 순서를 세심하게 바꾸는 것만으로 예외에 안전한 코드를 만들 수 있다.
    • 지금 코드에서는 pb를 바로 삭제하는 것이 아니라 포인터가 가리키는 객체를 복사한 직후에 삭제하는 것이다.
// Bitmap 객체 생성 과정에서 발생하는 예외 처리 
Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* pOrig = pb;                    // 원래의 pb를 어딘가에 복사
    pb = new Bitmap(*rhs.pb);            // 그 다음 pb가 *pb의 사본을 가리키게 만듬
    delete pOrig;                        // 원래의 pb 삭제

    return *this;
}
  • 예외 안전성과 자기대입 안전성을 동시에 가진 대입 연산자를 구현하는 방법은 복사 후 맞바꾸기 기법을 사용하는 것이다.
// 복사 후 맞바꾸기
class Widget {
    void swap(Widget& rhs);
};

Widget& Widget::operator=(const Widget& rhs) {
    Widget temp(rhs);                // rhs의 데이터에 대해 사본 생성
    swap(temp);                        // *this의 데이터를 사본의 것과 스왑
    return *this
}

// 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는 것이 가능함
// 값에 의한 전달을 수행하면 전달된 대상의 사본이 생김을 이용하는 방법
Widget& Widget::operator=(Widget rhs) {
    swap(this);
    return *this;
}

메모

항목 9

  • 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 말자. 가상 함수라고 해도 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래 쪽으로는 해당 정보를 넘겨줄 수 없기 때문이다.

항목 10

  • 대입 연산자는 *this의 참조자를 반환하도록 만들자. 이는 C++의 컨벤션이다.

항목 11

  • 대입 연산자(operator=)를 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들어야 한다. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 된다.
  • 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인해야 한다.

'책 > 이펙티브 C++' 카테고리의 다른 글

이펙티브 C++(121p ~ 132p)  (0) 2024.01.16
이펙티브 C++(111p ~ 121p)  (0) 2024.01.15
이펙티브 C++(89p ~ 99p)  (1) 2024.01.12
이펙티브 C++(80p ~ 89p)  (1) 2024.01.11
이펙티브 C++(71p ~ 79p)  (1) 2024.01.10
    '책/이펙티브 C++' 카테고리의 다른 글
    • 이펙티브 C++(121p ~ 132p)
    • 이펙티브 C++(111p ~ 121p)
    • 이펙티브 C++(89p ~ 99p)
    • 이펙티브 C++(80p ~ 89p)
    readme.md
    readme.md

    티스토리툴바