배움 저장소

[홍정모의 따라하며 배우는 C++] 12. 가상함수들 본문

Programming Language/C++

[홍정모의 따라하며 배우는 C++] 12. 가상함수들

시옷지읏 2021. 12. 28. 22:57

12.1 다형성의 기본 개념

자식객체가 상위 객체의 포인터를 사용하는 경우


자식 객체가 상위 객체의 포인터에 담겨 사용되는 경우 어떤 문제가 발생할까? 이 문제가 다형성과 관련있다

- 아래 예제에서 Cat 클래스는 Animal 클래스를 상속하였다. Cat 클래스가 오버라이딩한 멤버함수를 호출해보자. 아래에서 Cat Instance는 오버라이딩한 멤버함수를 호출한다.

- Cat Instance를 Animal 포인터 자료형에 담아보자. 오버라이딩한 멤버함수를 호출해보자. 자식객체에서 구현한 오버라이딩이 적용되지 않는다!

class Animal {
protected:
    string m_name;
public:
    Animal(string name): m_name(name){}
    string getName(){ return m_name; }
    void Roar() const {
        cout << m_name << " is undecided" << endl;
    }
};

class Cat : public Animal {
public:
    Cat(string name): Animal(name){}
    void Roar() const {
        cout << m_name << " Meow" << endl;
    }
};
int main() {
    Animal animal_1("anonymous");
    Cat kitty("kitty");
    animal_1.Roar();  // (Animal::) anonymous is undecided
    kitty.Roar();     // (Cat::)    kitty Meow 

    Animal *ptr = &kitty;
    ptr->Roar();      // (Animal::) kitty is undecided
}

아래에서 Cat과 Dog 객체가 Animal 객체를 상속받았다. 상속받은 객체는 상위객체의 멤버함수를 오버라이딩하였다. 이 때 오버라이딩한 멤버함수를 한 번에 호출할 수 있을까? Animal 객체로 변환하면 되지 않을까? 아래 예제를 보자.

class Animal {
protected:
    string m_name;
public:
    Animal(string name): m_name(name){}
    string getName(){ return m_name; }
    void Roar() const {
        cout << m_name << " is undecided" << endl;
    }
};

class Cat : public Animal {
public:
    Cat(string name): Animal(name){}
    void Roar() const {
        cout << m_name << " Meow" << endl;
    }
};

class Dog : public Animal {
public:
    Dog(string name) : Animal(name) {}
    void Roar() const {
        cout << m_name << " Bow Wow" << endl;
    }
};

int main() {
    Cat one("one"); Cat two("two"); Cat three("three");

    Dog first("first"); Dog second("second");
    
    Animal* ptr_array[] = { &one, &two, &three, &first, &second };
    for (int i = 0; i < 5; ++i)
        ptr_array[i]->Roar();
}
one is undecided
two is undecided
three is undecided
first is undecided
second is undecided

Animal 클래스에서 구현된 멤버함수가 호출되고 있다.

 

Animal의 멤버함수에 virtual keyword를 넣어주고 시도해보자. 다시 코드를 실행하면 오버라이딩 된 멤버함수가 호출된다! 상위 객체에서 호출하여도 상속받은 객체가 오버라이딩한 함수를 호출한다. 이를 다형성이라 한다.

class Animal {
protected:
    string m_name;
public:
    Animal(string name): m_name(name){}
    string getName(){ return m_name; }
    virtual void Roar() const { //========= ADD virtual !!
        cout << m_name << " is undecided" << endl;
    }
};
one Meow
two Meow
three Meow
first Bow Wow
second Bow Wow

 

12.2 가상 함수와 다형성

Virtual Functions and Polymorphism


virtual keyword가 붙은 함수를 Virtual Function(가상함수)라 부른다. 가상함수를 상속받은 객체에서 오버라이딩한다면 반환 자료형과 매개변수 자료형은 반드시 동일해야 한다. 다르면 에러가 발생한다!

 

가상함수가 자신에게 맞는 객체의 오버라이딩을 찾아갈 때 스택을 사용하지 않는다. 별도로 작성된 테이블을 활용하여 자신과 맞은 오버라이딩을 찾아간다. 스택보다 느리다. 함수가 빈번하게 호출된다면 가상함수는 적절하지 않다.

 

자식 객체에서 다시 한 번 더 상속을 해보자. 할머니 객체, 어머니 객체, 본인 객체로 비유할 수 있다. 이 때 최상위 객체에서 정의된 가상함수를 활용해보자. 할머니 객체 참조자와 어머니 객체 참조자에 본인 객체를 넣어 오버라이딩 함수를 호출하면 어떤 클래스에 속한 멤버함수가 호출될까?

class Grand {
public:
    virtual void print() { cout << "Grand" << endl; }
};

class Parent: public Grand {
public:
    void print() { cout << "Parent" << endl; }
};

class Me: public Parent {
public:
    void print() { cout << "Me" << endl; }
};

아래 예제는 모두 자기 클래스에 맞는 멤버함수가 호출됨을 보여준다. 최상위 객체에서만 가상함수가 선언되었다는 걸 확인해보자. 가상함수가 선언된 클래스의 바로 아래 클래스만 가상함수를 사용하지 않을까? 의문을 품을 수 있다. 그렇지 않다. 최상위 클래스가 가상함수를 선언하면 그 아래 상속하고 있는 모든 클래스는 가상함수를 이어받는다. 자기 클래스에 맞는 멤버함수를 호출하고 다형성을 구현한다.

int main() {
    Grand grand; 
    Parent parent; 
    Me me;

    Grand& g_ref1 = grand;
    Grand& g_ref2 = parent;
    Grand& g_ref3 = me;
    g_ref1.print(); // Grand
    g_ref2.print(); // Parent
    g_ref3.print(); // Me
}
Parent& p_ref1 = parent;
Parent& p_ref2 = me;
p_ref1.print(); // >> Parent
p_ref2.print(); // >> Me

이러한 성질을 구체적으로 표시하기 위하여 오버라이딩하고 있는 모든 멤버함수에 virtual keyword를 붙이기도 한다

class Grand {
public:
    virtual void print() { cout << "Grand" << endl; }
};

class Parent: public Grand {
public:
    virtual void print() { cout << "Parent" << endl; }
};

class Me: public Parent {
public:
    virtual void print() { cout << "Me" << endl; }
};

위 코드를 보자. 최상위 객체 Grand에서 virtual keyword를 빼버리면 어떻게 될까? Grand 참조자 혹은 포인터는 더이상 다형성을 구현하지 못하고 자신의 멤버함수만 호출하게 된다.

 

12.3 override, final, 공변 반환값

                                 Covariant(공변)


override keyword

멤버 함수가 상위 객체의 가상함수를 오버라이딩한다면 함수 정의에 override keyword를 붙여주자. 상위 객체와 동일한 형태를 가지고 있는지 compile time에 검사된다. 아래 B의 멤버함수의 반환값이나 자료형을 변화시키면 에러가 발생한다.

class A {
public:
    virtual void print(int x) { cout << "A" << endl; }
};

class B: public A {
public:
    virtual void print(int x) override { cout << "B" << endl; }
};

 

final keyword

final keyword를 매개함수에 넣으면 하위 객체가 해당 매개함수를 오버라이딩 할 수 없다. final keyword는 하위 객체의 함수 오버라이딩을 금지한다. 아래 예제에서 클래스 B 매개함수에 final을 지정하자 C에서 오버라이딩 불가 에러가 발생하였다.  'B::print': function declared as 'final' cannot be overridden by 'C::print'

class A {
public:
    virtual void print(int x) { cout << "A" << endl; }
};

class B: public A {
public:
    virtual void print(int x) final { cout << "B" << endl; }
};

class C: public B{
public:
    virtual void print(int x) override{ cout << "C" << endl; } //Error!
};

 

Covariant Return type

- 오버라이딩한 함수는 형태가 동일해야 한다. 예외가 있는데 자기 자신의 주소를 반환할 때이다. 자기 자신의 참조자나 포인터를 반환할 때는 반환되는 자료형이 달라도 오버라이딩이 허용된다. 예제는 다음과 같다. 각 클래스는 자신의 주소를 반환하는 매개함수를 가진다.

class A {
public:
    virtual A* getThis() {
        cout << __FUNCTION__ << " is working " << endl;
        return this;
    }
};

class B : public A {
public:
    virtual B* getThis() {
        cout << __FUNCTION__ << " is working " << endl;
        return this;
    }
};

int main() {
    B b;
    A& ref = b;
    ref.getThis();

    cout << typeid(ref.getThis()).name() << endl;
}

위 코드를 실행시켜보자. 신기한 일이 일어난다. getThis함수를 호출하면 상속받은 객체에서 오버라이딩 된 함수가 호출된다. 다형성이 구현되었다. 이상한건 typeid로 해당 함수의 반환값이 B*가 아니라 A*라는 점이다.

B::getThis is working
class A *

왜 이런 결과가 나왔을까?

다형성이 구현되어 getThis 함수가 B*를 반환하였지만 ref 내부에서 A*로 변환하였다

 

typeid 내부에서 호출한 getThis 함수가 실행되지 않았다

- typeid는 인자가 L-value이고 다형성으로 구현되었을 때 해당 함수를 실행한다. 그렇지 않을 경우 반환값만 확인한다.

https://en.cppreference.com/w/cpp/language/typeid

 

typeid operator - cppreference.com

Queries information of a type. Used where the dynamic type of a polymorphic object must be known and for static type identification. [edit] Syntax typeid ( type ) (1) typeid ( expression ) (2) The header must be included before using typeid (if the header

en.cppreference.com

 

12.4 가상 소멸자


다형성을 구현하면 상속받은 클래스를 상위 클래스 자료형으로 관리할 수 있다.

이 때 동적할당메모리를 사용하면 문제가 발생할 수 있다

- 상위 클래스에서 소멸자가 호출되어도 상속받은 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생할 수 있다.

class Base {
public:
    ~Base() {
        cout << __FUNCTION__ << endl;
    }
};

class Derived : public Base {
private:
    int *m_array;
public:
    Derived(int length) {
        m_array = new int[length];
    }
    ~Derived() {
        cout << __FUNCTION__ << " is freeing dynamic memory" << endl;

        delete [] m_array;
    }
};

int main() {
    // Work with polymorphism
    // It's normal that a class manage every interitance class
    Derived *derived = new Derived(5);
    Base *base = derived;
    delete base; // call base's destructor, memory leak
}

해결방법은 간단하다. 상위클래스 소멸자 함수에 virtual keyword를 추가해주자. 상위 클래스 소멸자가 상속받은 클래스의 소멸자를 호출한다.

class Base {
public:
    virtual ~Base() { // ... }
};
Derived::~Derived is freeing dynamic memory
Base::~Base

이 때 상속받은 객체에 override keyword를 붙여주자

class Derived : public Base {
public:
    ~Derived() override { // ... }
};

 

12.5 동적 바인딩과 정적 바인딩


다형성은 가상함수로 구현된다. 가상함수는 어떤 원리로 동작할까? 동적 바인딩과 정적 바인딩을 알아보자.

 

static binding(정적 바인딩)

- 아래 예제는 정적 바인딩을 구현하였다. 컴파일 타임에 모든 식별자(변수명과 함수명)이 결정되었을 때 정적 바인딩이라 한다. static binding 혹은 early binding이라고도 부른다.

int add(int x, int y) { return x + y; }
int subtract(int x, int y) { return x - y; }
int multiply(int x, int y) { return x * y; }

int main() {
    int x = 1, y = 2;
    int op = 2;
    int result;

    switch (op) {
        case 0: result = add(x,y); break;
        case 1: result = subtract(x,y); break;
        case 2: result = multiply(x,y); break;
    }
    cout << result << endl;
}

 

Dynamic binding(동적 바인딩)

- 아래 예제는 동적 바인딩을 구현하였다. 컴파일 타임에 변수나 함수가 결정되어있지 않고 런타임에 결정되는 것을 동적 바인딩이라 한다. late binding이라고도 부른다. 아래 예제에서 함수 포인터가 동적 바인딩의 핵심이다.

int add(int x, int y) { return x + y; }
int subtract(int x, int y) { return x - y; }
int multiply(int x, int y) { return x * y; }

int main() {
    int x = 1, y = 2;
    int op = 2;
    int result;

    int(*func_ptr)(int, int) = nullptr;
    switch (op) {
        case 0: func_ptr = add; break;
        case 1: func_ptr = subtract; break;
        case 2: func_ptr = multiply; break;
    }
    cout << func_ptr(x,y) << endl;
}

 

정적 바인딩 vs 동적 바인딩

ㄴ정적 바인딩이 빠르다. 동적 바인딩은 함수 포인터에 찾아가 실행시킨다. 자주 실행되는 함수는 정적으로 구현하자

ㄴ동적 바인딩을 사용하면 코드 관리가 쉬워진다.

 

12.6 가상 (함수) 표

Virtual Tables


다형성은 동적 바인딩으로 구현되는데 여기에 가상함수표가 더 필요하다. 가상함수가 어느 클래스의 override 멤버함수를 호출해야 할지 선택해야하기 때문이다. 그래서 내부에 가상(함수)표를 참고한다. 

 

Base 클래스에서 가상함수표가 사용되는 과정

(1) 가상함수가 선언되면 virtual table(가상함수표)가 작성되고 해당 클래스 내부에 가상함수표를 가리키는 포인터가 생성된다. (포인터 멤버변수가 추가되어 저장공간이 늘어난다). 이 포인터를 사용하여 가상함수표로 이동한다. 

(2) 가상함수는 포멧(자료형과 식별자)가 동일해야 하므로 포멧으로 서로를 구분할 수 있다. 가상함수표는 각 가상함수 포멧마다 포인터를 하나씩 가지고 있다. 

(3) 가상함수를 호출하면 가상함수표로가 자기 포멧과 맞는 함수포인터로 이동한다. 그리고 그 함수를 실행시킨다.

 

Derived 클래스에서 가상함수표가 사용되는 과정

(1) 상위 클래스가 가상함수를 선언했다면 상속받은 함수도 가상함수표를 참고해야 한다. 가상함수 표를 가리키는 포인터를 멤버 변수로 가진다.

(2) 가상함수표에 방문하여 호출한 멤버함수를 찾아간다

(3)-1 오버라이딩이 구현되었다면 해당 클래스의 함수를 호출한다.

(3)-2 상속받은 함수를 호출하였다면 상위 클래스의 함수를 찾아가 호출한다.

 

12.7 순수 가상 함수, 추상 기본 클래스, 인터페이스 클래스

Pure, Abstract, Interface


상위 객체가 상속받을 객체의 특징을 미리 정할 수 있다. 순수가상함수, 추상기본클래스, 인터페이스클래스로 구현한다.

 

Pure virtual function(순수가상함수)   : body가 없어 상속받을 객체가 반드시 구현해야 한다.

Abstract base class(추상 기본 클래스) : 순수가상함수를 멤버함수로 가지고 있는 클래스이다.

Interface class(인터페이스 클래스)     : 모든 멤버함수가 순수가상함수인 클래스이다.

 

Pure Virtual Function(순수가상함수)

순수가상함수는 body없이 초기화한다

- 해당 함수에 값 0을 할당하자. 반드시 상속받을 객체가 이를 구현해야 한다.

- 순수가상함수가 정의된 클래스는 인스턴스를 만들 수 없다.

- 아래 코드는 에러를 발생시킨다. 'void Animal::Roar(void) const': is abstract

class Animal {
protected:
    string m_name;
public:
    Animal(string name) : m_name(name) {}
    string getName() { return m_name; }
    virtual void Roar() const = 0; // Pure virtual function
};

int main() {
    Animal anonym("anonymous"); // Error!
}

 

순수가상함수를 상속받은 클래스는 이를 반드시 구현하여야 한다

- Cat 클래스는 순수가상함수를 구현하였지만 Dog는 구현하지 않았다. Dog로 인스턴스를 만들면 에러가 발생한다.

Error: object of abstract class type "Dog" is not allowed: pure virtual function "Animal::Roar" has no overrider

class Animal {
protected:
    string m_name;
public:
    Animal(string name) : m_name(name) {}
    string getName() { return m_name; }
    virtual void Roar() const = 0; // Pure virtual function
};

class Cat : public Animal {
public:
    Cat(string name) : Animal(name) {}
    void Roar() const {
        cout << m_name << " Meow" << endl;
    }
};

class Dog : public Animal {
public:
    Dog(string name) : Animal(name) {}
    // override Pure virtual function or Erorr!
};

int main() {
    Cat kitty("Kitty");
    Dog bark("Bark"); // Error!
}

 

Interface Class

인터페이스 클래스는 모든 멤버함수가 순수함수이다.

- 인터페이스 클래스의 예제이다. IErrorLog는 인터페이스 클래스로 인스턴스를 만들 수 없다. 인터페이스 클래스를 상속받는 모든 클래스는 가상함수를 반드시 구현하여야 한다. 아래 코드에서 주석을 해제하면 에러가 사라진다.

class IErrorLog { // prefix I for interface class
public:
    virtual bool reportError(const char* errorMessage) = 0;
};

class FileErrorLog : public IErrorLog{
public:
    bool reportError(const char* errorMessage) override {
        cout << "Writing error to a file" << endl;
        return true;
    }
};

class ConsoleErrorLog : public IErrorLog {
public:
    //bool reportError(const char* errorMessage) override {
    //    cout << "Printing error to a Console" << endl;
    //    return true;
    //}
};

int main() {
    ConsoleErrorLog console_Elog; // Error!
}

 

인터페이스 클래스 활용하기

- 상속받은 여러 객체를 한 번에 사용할 수 있다. 상속받은 객체별 기능을 신경쓰지 않고 사용하기 편하다.

// Interface know how to work, it introduce member function.
void ReportError(IErrorLog& log) { log.reportError("Runtime Error"); }

int main() {
    FileErrorLog file_Elog;
    ConsoleErrorLog console_Elog;
    ReportError(file_Elog);   // >> Writing error to a file
    ReportError(console_Elog);// >> Printing error to a Console
}

 

12.8 가상 기본 클래스와 다이아몬드 상속 문제

Virtual base classes, The diamond problem


아래는 The diamond problem(다이아몬드 상속 문제)를 보여준다. 왼쪽그림이 프로그래머가 의도한 설계이지만 실제 코드를 작성하여 실행하면 오른쪽과 같은 형태가 만들어진다. B1과 B2 모두 최상위 인스턴스 A에서 갈라져나와야 하지만 각각의 인스턴스를 만들고 있음을 알 수 있다.

 

왼쪽 그림처럼 프로그래머의 의도를 구현하기위해 아래과 같이 코드를 작성해주자

다음은 코드로 구현된 다이아몬드 문제이다. 최상위 인스턴스는 서로 다른 주소를 가진다

class PoweredDevice {
public:
    int m_power;
    PoweredDevice(int power) : m_power(power) {
        cout << __FUNCTION__ << ":" << m_power << endl;
    }
};
class Scanner : public PoweredDevice{
public:
    Scanner(int scanner, int power) : PoweredDevice(power){
        cout << __FUNCTION__ << ":" << scanner << endl;
    }
};
class Printer : public PoweredDevice {
public:
    Printer(int scanner, int power) : PoweredDevice(power) {
        cout << __FUNCTION__ << ":" << scanner << endl;
    }
};
class Copier: public Scanner, public Printer{
public:
    Copier(int s, int pri, int pow) : Scanner(s,pow), Printer(pri,pow) {}
}; 

int main() {
    Copier copier(0,1,2);
    cout << &copier.Scanner::PoweredDevice::m_power << endl; // 0044FCFC
    cout << &copier.Printer::PoweredDevice::m_power << endl; // 0044FD00
}

 위 문제를 해결하기 위해 가상기본 클래스를 만들자. 최상위 클래스 바로 아래 클래스에 virtual keyword를 더해주고 맨 아래 클래스의 생성자 initializer list에 최상위 클래스의 생성자를 구현하자.

 손자 클래스가 할아버지 생성자를 만들어 버리면 두 부모는 할아버지 생성자를 호출하지 않도록 처리가 된다. 

class Scanner : virtual public PoweredDevice{ /* ... */ };
class Printer : virtual public PoweredDevice { /* ... */ };

class Copier: public Scanner, public Printer{
public:
    Copier(int s, int pri, int pow) : Scanner(s,pow), Printer(pri,pow), PoweredDevice(pow) {}
};

동일한 주소가 출력된다. 가상기본 클래스를 구현하여 다이아몬드 문제를 해결하였다.

Copier copier(0,1,2);
cout << &copier.Scanner::PoweredDevice::m_power << endl; // 003DF9E8
cout << &copier.Printer::PoweredDevice::m_power << endl; // 003DF9E8

 

12.9 객체 잘림과 reference wrapper

Object slicing,


자식 클래스는 부모 클래스를 포함한다. 자식 클래스는 부모 클래스보다 크기가 크다. 만약 부모 클래스에 자식 클래스를 저장하면 어떻게 될까? 부모 클래스의 크기는 자식 클래스보다 작다. 그래서 데이터가 날아간다.

reference_wrapper를 사용하여 데이터가 날아감을 방지할 수 있다

 

Object slicing(객체잘림)

부모 클래스 자료형에 자식 클래스 정보를 대입하면 자식 클래스에 구현된 정보가 날아간다.

class Base {
public:
    int m_i = 0;
    virtual void print(){
        cout << __FUNCTION__ << endl;
    }
};

class Derived : public Base {
public:
    int m_j = 1;
    void print() override{
        cout << __FUNCTION__ << endl;
    }
};

int main() {
    Derived derived;
    Base &b_ref = derived;
    
    /* polymorphism works */ 
    b_ref.print();  // >> Derived::print

    /* polymorphism not works, truncation occurr */
    Base copied = derived;
    copied.print(); // >> Base::print
}

- 부모 클래스 자료형을 사용한 참조자해당 주소값이 추가적인 정보를 가지고 있음을 알게된다. 이를 통해 다형성을 구현할 것이다. 해당 클래스에 맞는 오버라이딩 멤버함수를 호출한다. 

- 클래스 자료형에 자식 클래스 값을 복사하여 넣고 있다. 이 때 부모 클래스 자료형은 자식 클래스 자료형보다 작으므로 자식 클래스의 값이 날아가버린다. 이 때 다형성은 구현되지 않는다.

 

함수 내부에서도 동일한 원리가 적용된다

void doPolymorphism(Base& ref) { ref.print(); }
void noPolymorphism(Base copied) { copied.print(); }

int main() {
    Derived derived;
    
    doPolymorphism(derived); // Derived::print
    noPolymorphism(derived); // Base::print
}

 

vector의 자료형에 따라 다형성이 구현될지 말지가 달라진다

- 상위 클래스 자료형에 상속된 클래스 자료형 값을 복사할 경우을 object slicing(객체잘림) 현상이 발생한다. 다형성을 구현하기위하여 포인터를 사용함이 적절하다. 

int main() {
    Derived derived;
    Base base;

    vector<Base> v_copied;
    v_copied.push_back(derived);
    v_copied.push_back(base);
    v_copied[0].print(); // Base::print
    v_copied[1].print(); // Base::print
        
    vector<Base*> v_pointer;
    v_pointer.push_back(&derived);
    v_pointer.push_back(&base);
    v_pointer[0]->print(); // Derived::print
    v_pointer[1]->print(); // Base::print
}

위 예제에서 벡터는 자료형으로 기본 자료형과 포인터를 사용하였다. vector는 해당 값을 변경할 수 있어야 하므로 참조 자료형을 금지한다. 레퍼런스를 사용할 수 있을까?

 

<functional>내부에서 구현된 reference_wrapper<{type}>을 사용해보자. standard library에서 참조자 기능을 구현해놓았다. 벡터 내부에서도 사용할 수 있다. 해당 값을 불러오기 위해 멤버함수 .get( ) 을 호출하면 된다.

 아래 예제에서 reference_wrapper를 사용하여 다형성을 구현하였다.

#include <functional>
int main() {
    Derived derived;
    Base base;

    vector<std::reference_wrapper<Base>> v_ref;

    v_ref.push_back(derived);
    v_ref.push_back(base);

    v_ref[0].get().print(); // Derived::print
    v_ref[1].get().print(); // Base::print
}

 

12.10 동적 형변환


부모 클래스 포인터 자료형에 담아 사용하다가 자기자신 포인터로 돌아가고 싶을 때 동적 형변환을 쓰자

- 자신의 클래스에서만 접근가능한 멤버를 사용하고 싶을때 유용하다

- dynamic_cast<클래스 자료형*>( 변수명 )을 사용한다.

class Base {
public:
    int m_i = 0;
    virtual void print(){
        cout << __FUNCTION__ << endl;
    }
};

class Derived1: public Base {
public:
    int m_j = 10;
    void print() override{
        cout << __FUNCTION__ << endl;
    }
};

class Derived2 : public Base {
public:
    string m_name = "Two";
    void print() override {
        cout << __FUNCTION__ << endl;
    }
};

int main() {
    Derived1 derived;
    Base *base = &derived;

    auto *auto_ptr = dynamic_cast<Derived1*>(base);
    Derived1 *derived_ptr = dynamic_cast<Derived1*>(base);
}

동적 형변환은 디버깅이 어렵고 코드를 읽기도 어렵다. 가급적이면 피함이 좋다.

 

dynamic_cast는 형변환을 실패하면 nullptr를 반환한다

- nullptr의 멤버에 접근하면 런타임에 프로그램이 중단된다. if 조건문을 사용하여 항상 확인해주자.

Derived1 derived;
Base *base = &derived;

auto *auto_ptr = dynamic_cast<Derived2*>(base);
if (auto_ptr == nullptr)
    cout << "auto_ptr is nullptr!!" << endl;
>> auto_ptr is nullptr!!

 

static_cast는 형변환을 어떻게든 실행한다

- Derived2*로 형변환하고 해당 멤버 변수 m_name를 출력하면 컴파일은 되지만 실행 중 프로그램이 중단된다.

Derived1*의 멤버 변수 m_j에는 접근이 불가능하다. 컴파일이 되지 않는다. 

Derived2 *auto_ptr = static_cast<Derived2*>(base);
if (auto_ptr == nullptr)
    cout << "auto_ptr is nullptr!!" << endl;
else {
    auto_ptr->print();
    cout << auto_ptr->m_name << endl;
}
>> Derived1::print
>> (Program Stop at here)

 

static casting(정적 형변환)과 dynamic casting(동적 형변환)

- static_cast를 사용하면 존재하지 않는 멤버에 접근하게 되어 런타임 에러가 발생할 수 있다.

- dynamic_cast를 사용해도 nullptr에서 멤버 변수를 호출하여 런타임 에러가 발생할 수 있다.

- dynamic_cast를 사용하고 nullptr를 확인해주면 안전하다.

- 동적 형변환은 되도록이면 쓰지 않음이 좋다. functional 혹은 lambda functional로 대체할 수 있다.

 

12.11 유도 클래스에서 출력 연산자 사용하기


출력연산자 오버로딩 구현해보기

- 출력 연산자는 오버라이딩이 불가능하여 다형성을 구현하기 어렵다. 이 때 오버라이딩이 불가능 함수는 임시 함수 혹은 위임받을 함수를 만들어 오버라이딩하면 된다. 오버라이딩이 불가능한 함수 내부에서 임시 함수를 호출하여 다형성을 구현해주자.

class Base {
public:
    Base(){}
    virtual ostream& print(ostream& out) const {
        cout << __FUNCTION__;
        return out;
    }
    friend ostream& operator << (ostream& out, const Base& b) {
        return b.print(out); // polymorphism works at here
    }
};

class Derived : public Base {
public:
    Derived(){}
    virtual ostream& print(ostream& out) const override{
        cout << __FUNCTION__;
        return out;
    }
};

int main() {
    Base b;
    cout << b << endl;

    Derived d;
    cout << d << endl; // Works without Error!

    Base& ref = d;
    cout << ref << endl;
}
Base::print
Derived::print
Derived::print

Derived 클래스에서 << 연산자 오버라이딩을 구현하지 않았는데도 문제없이 작동함을 알 수 있다. 부모 클래스의 연산자 오버로딩을 사용하였다. 

Comments