/ c++

C++ Tips - 1

Comments

본 포스트는 Effective C++ item 9~11 를 참고하여 작성함.

객체 생성 및 소멸 과정 중에는 virtual 함수를 부르면 안됨!
  • 호출 결과가 생각했던것과 같이 작동하지 않거나, 만약 작동 한다고 하더라도 문제가 생길 여지가 큼
  • C++ 만의 특징(자바나 C#이랑 다르게 행동함)

문제 상황 1: 로깅을 간단히하는 주식 프로그램

class Transaction{ // 모든 거래에 대한 기본 클래스
public:
    Transaction(); 
    virtual void logTransaction() const = 0; // 타입에 따라 달라지는 로그 기록
    ...
};
 
Transaction::Transaction(){ // 기본 클래스 생성자의 구현
    ...
    logTransaction(); // 해당 거래를 로깅(하기 시작) 함
}
 
class BuyTransaction: public Transaction{
public:
    virtual void logTransaction() const; // Buy 거래 타입에 따른 로깅 구현
    ...
};
 
class SellTransaction: public Transaction{
public:
    virtual void logTransaction() const; // Sell 거래 타입에 따른 로깅 구현
    ...
};
 
BuyTransaction b; // 해당 코드가 실행 될 때, 어떻게 될까?
  • 어떻게 될까요???

  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 사실 링커에러남... (g++ 7.3.1 기준)

 warning: pure virtual ‘virtual void Transaction::logTransaction()’ called from constructor
 undefined reference to `Transaction::logTransaction()'
  • 이유는 생성자 호출 순서: Transaction() --> BuyTransaction()
  • Transaction() 생성자가 호출 될 때, 가상 함수 logTransaction()의 구현으로 BuyTransaction class의 것이 아니라, Transaction의 것을 사용
    • 기본 클래스의 생성자가 호출될 동안에는, 가상 함수는 절대 파생 클래스 쪽으로 내려가지 않음
    • C++는 초기화 되지 않은 데이터 멤버를 건드릴 수 있는 파생 클래스의 가상함수 호출을 원천 차단함
      • 초기화 되지 않은 데이터를 건드리는 것 = 갑자기 '미정의 동작'행 직행 열차표가 손에 들려있고, 야근 기본 무한 짜증의 디버깅 모드로 돌입할 수 있음

중요한 사실: 파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체 타입은 기본 클래스가 됨 (e.g. dynamic_case)

더 짜증나는 경우: 간접 호출

class Transaction{ // 모든 거래에 대한 기본 클래스
public:
    Transaction(){
        init(); // 비가상 멤버 함수 호출
    }
    virtual void logTransaction() const = 0;
    ...
private:
    void init(){
        ...
        logTransacction(); // 비가상 함수에서 가상 함수 호출
 
    }
};
  • 이전 코드는 컴파일러가 경고 메시지를 발생시키기도 하지만, 해당 코드는 컴파일/링크도 잘 된 후에 순수가상함수가 런타임에 호출 될 때 프로그램을 종료시킴
    • 이전 코드는 컴파일러에 따라 아예 에러를 발생시키기도 함
  • 만약 Transaction에 logTransaction() 함수가 구현되어 있다면, 프로그램은 잘 돌아가겠지만 문제가 생겼을 때 원인 파악이 매우 힘듬

해결 방법 예시.

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 BuyTansaction: public Transaction{
public:
    BuyTransaction( parameters )
        : Transaction(createLogString( parameters) )
    { ... }
    ...
private:
    static std::string createLogString( parameters );
};
  • 여러 방법 중 하나임 - 기본 클래스의 생성자에 가상 함수를 사용하지 않고, 직접 정보를 전달하기
    • 기본 클래스의 생성자는 비가상 함수를 안전하게 호출할 수 있게 됨
    • 대신 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 '올려' 주도록 만들기
    • createLogString: static함수이기 때문에, 생성이 끝나지 않은 객체의 미초기화된 데이터 멤버를 건드릴 위험이 없음
  • 이것만은 기억 하기: 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 말자. 가상 함수라고 해도, 현재 실행 중인 생성자나 소멸자에 해당하는 클래스의 파생 클래스 쪽으로는 내려가지 않음.
대입 연산자는 *this의 참조자를 반환하게 하자
int x, y, z;
x = y = z = 15; 
x = (y = (z = 15)); // 위 코드와 동일
  • C++의 대입 연산의 특성: 우측 연관(right-associative) 연산
  • 이런 구현은 일종의 관례(convention) 이지만, 새로 만드는 클래스 에서도 해당 관례를 지키는 것이 좋음
class Widget{
public:
    ...
    Widget& operator=(const Widget& rhs){ // +=, -=, *= 등에도 동일한 convention이 적용
        ...
        return *this; // 좌변 객체의 참조자를 반환
    }
    Widget& operator=(const int rhs){ // 매개변수 타입이 일반적이지 않은 경우에도 동일한 규약을 적용
        ...
        return *this; // 좌변 객체의 참조자를 반환
    }
    ...
};
  • 이것만은 기억하기: 대입 연산자는 *this의 참조자를 반환하도록 만들기
operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자
class Widget { ... };
 
Widget w;
...
 
w = w; // 자기대입(self assignment)
------------
a[i] = a[j]; // 자기대입의 가능성이 있는 문장 1
*px = *py; // 자기대입의 가능성이 있는 문장 2
  • 여러 곳에서 하나의 객체를 참조하는 중복참조(aliasing)으로 인해 자기대입이 발생함

자기대입 문제 상황과 전통적인 대책

class Bitmap { ... };
 
class Widget{
public:
    ...
    Widget& operator=(const Widget& rhs);
private:
    Bitmap *pb; // Heap에 할당된 객체를 가리키는 포인터
};
 
Widget& Widget::operator=(const Widget& rhs){
    delete pb;
    pb = new Bitmap(*rhs.pb);
 
    return *this;
}
  • 만약 대입되는 대상(*this)와 rhs가 같은 객체라면, delete 연산자가 rhs 객체까지 적용됨
  • 전통적인 대책은 operator=의 첫머리에서 일치성 검사(identity test)를 통해 자기대입을 점검하는 것
Widget& Widget::operator=(const Widget& rhs){
    if (this == &rhs) return *this; // 자기대입이라면 아무것도 하지 않음
 
    delete pb;
    pb = new Bitmap(*rhs.pb);
 
    return *this;
}
  • 하지만 위의 코드는 예외에 대해서 안전하지 않음 (예외 안전성은 item 29에서 다시 나옴)
  • 'new Bitmap' 에서 예외가 생긴다면, Widget 객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가지게 됨
  • 좋은소식은 operator= 을 예외에 안전하게 구현하면 대개 자기대입에도 안전한 코드가 나옴

순서바꾸기: 예외에 안전한 operator=

Widget& Widget::operator=(const Widget& rhs){
    Bitmap *pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
 
    return *this;
}
  • 예외에 안전하면서도, 자기대입 문제가 발생하지 않음
  • 효율? 일치성 검사 vs. 순서 바꾸기
    • 일치성테스트가 new & delete 보다 반드시 효율이 좋다고 할 수 없음: code size, branch, instruction prefetch, cash, pipelining (어차피 얼마 차이 없어서 그게 그거인거 같은데 책에 이렇게 언급되어있음)

순서바꾸기: 예외에 안전한 operator=

class Widget{
    ...
    void swap(Widget& rhs);
    ...
};
Widget& Widget::operator=(const Widget& rhs){
    Widget temp(rhs); // rhs 사본 생성
    swap(temp); // *this의 데이터를 사본과 맞바꿈
    return *this;
}
  • operator= 작성에 아주 자주 쓰이는 방식
  • 자기대입은 물론 예외에 안전함 (예외 안정성과 관련이 있어서 항목 29에 자세히 나와있음)
Widget& Widget::operator=(Widget rhs){
    swap(rhs); // *this의 데이터를 사본과 맞바꿈
    return *this;
}
  • C++의 두가지 특징을 활용한 약간 다른 구현
    • (1) 클래스의 복사 대입 연산자는 인자를 값으로 취하도록 선언하는것이 가능
    • (2) 값에 의한 전달(call by value)을 수행하면 사본이 생김

이것만은 기억하기

  1. operator= 을 구현할 때 자기대입이 발생하는 경우를 제대로 처리하도록 만들기
  2. 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 같은 객체인 경우에도 정확하게 동작하는지 확인하기