18.2 Virtual functions and polymorphism

주홍영·2022년 3월 21일
0

Learncpp.com

목록 보기
173/199

https://www.learncpp.com/cpp-tutorial/virtual-functions/

우리는 이전에 parent class의 pointer와 reference를 사용해 코드를 좀 더 간소화 시킬 수 있다는 것을 배웠다. 하지만 child class의 member function이 아닌 parent class의 member function이 호출되는 것에 대한 문제에 직면했다

이번 레슨에서는 virtual function을 이용해 이러한 이슈를 어떻게 다루는지 살펴보자

Virtual functions and polymorphism

virtual function은 함수의 특별한 타입이다.
호출되면 Base 클래스와 Derived 클래스 사이에 존재하는 함수 중 most-derived 버젼으로 resolve 하는 (해석이 너무 아리송하네요..)
이러한 기능을 polymorphism(다형성)이라고 한다.
derived function은 same signature(name, parameter types, whether it is const)와 return type이 일치하면 match된 것으로 간주된다.
위와 같은 함수를 overrides라고 부른다

function을 virtual로 만들고 싶으면 간단하게 virtual 키워드를 사용하면 된다

예시를 살펴보자

#include <iostream>
#include <string_view>

class Base
{
public:
    virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword
};

class Derived: public Base
{
public:
    virtual std::string_view getName() const { return "Derived"; }
};

int main()
{
    Derived derived;
    Base& rBase{ derived };
    std::cout << "rBase is a " << rBase.getName() << '\n';

    return 0;
}

위 코드의 출력은 다음과 같다

rBase is a Derived

rBase는 derived object의 Base reference이다 그러나 rBase.getName()이 호출되었을 때 Derived 버젼의 getName()이 호출되었음을 알 수 있다
virtual로 설정했기 때문에 Base와 Derived 사이에서 찾은 것이다

좀 더 복잡한 example을 살펴보자

#include <iostream>
#include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B: public A
{
public:
    virtual std::string_view getName() const { return "B"; }
};

class C: public B
{
public:
    virtual std::string_view getName() const { return "C"; }
};

class D: public C
{
public:
    virtual std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    A& rBase{ c };
    std::cout << "rBase is a " << rBase.getName() << '\n';

    return 0;
}

위 코드의 출력은 다음과 같다

rBase is a C

main에서 C클래스 object를 만들었고 A ref타입으로 전달했따
그리고 getName()함수를 호출했다
만약 virtual이 아니라면 A의 member function을 호출했겠지만
A의 member function이 virtual 이므로 A와 C사이에 가장 derived된 함수를 찾아서 호출했다

A more complex example

더 복잡한 example을 살펴보자

#include <iostream>
#include <string>
#include <string_view>

class Animal
{
protected:
    std::string m_name;

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(const std::string& name)
        : m_name{ name }
    {
    }

public:
    const std::string& getName() const { return m_name; }
    virtual std::string_view speak() const { return "???"; }
};

class Cat: public Animal
{
public:
    Cat(const std::string& name)
        : Animal{ name }
    {
    }

    virtual std::string_view speak() const { return "Meow"; }
};

class Dog: public Animal
{
public:
    Dog(const std::string& name)
        : Animal{ name }
    {
    }

    virtual std::string_view speak() const { return "Woof"; }
};

void report(const Animal& animal)
{
    std::cout << animal.getName() << " says " << animal.speak() << '\n';
}

int main()
{
    Cat cat{ "Fred" };
    Dog dog{ "Garbo" };

    report(cat);
    report(dog);

    return 0;
}

virtual로 Animal의 speak 함수를 지정했다
따라서 우리는 report function이 Animal ref로 argument를 넘겨 받더라도
의도한 동작을 할 수 있을 거라고 생각한다
출력은 다음과 같다

Fred says Meow
Garbo says Woof

의도한 대로 동작함을 확인할 수 있다

비슷하게 우리는 다음과 같은 array example도 확인할 수 있다

Cat fred{ "Fred" };
Cat misty{ "Misty" };
Cat zeke{ "Zeke" };

Dog garbo{ "Garbo" };
Dog pooky{ "Pooky" };
Dog truffle{ "Truffle" };

// Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
Animal* animals[]{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };

for (const auto *animal : animals)
    std::cout << animal->getName() << " says " << animal->speak() << '\n';

출력은 다음과 같다

Fred says Meow
Garbo says Woof
Misty says Meow
Pooky says Woof
Truffle says Woof
Zeke says Meow

참고로 어느 한 matching function에 virtual 키워드만 존재한다면 virtual로 간주된다

예를 들어

#include <iostream>
#include <string_view>

class A
{
public:
    virtual std::string_view getName() const { return "A"; }
};

class B : public A
{
public:
    std::string_view getName() const { return "B"; }
};

class C : public B
{
public:
    virtual std::string_view getName() const { return "C"; }
};

class D : public C
{
public:
    virtual std::string_view getName() const { return "D"; }
};

int main()
{
    C c;
    B& rBase{ c };
    std::cout << "rBase is a " << rBase.getName() << '\n';

    return 0;
}

B ref로 받았는데 B클래스에서 virtual 키워드를 설정하지는 않았다
하지만 A클래스에서 virtual로 키워드가 설정되어 있어 virtual화 되어 있다
따라서 출력은

rBase is a C

그런데 만약 A클래스의 virtual 마저 사라진다면 B입장에서는 virtual의 힌트를 얻을 수가 없으므로 이 경우에 출력은

rBase is a B

가된다

Return types of virtual functions

return type은 match에 중요한 요소이다
즉, return type이 다르면 다름 함수라고 생각해도 된다

class Base
{
public:
    virtual int getValue() const { return 5; }
};

class Derived: public Base
{
public:
    virtual double getValue() const { return 6.78; }
};

위의 경우에는 compile error가 발생한다

그리고 return type만으로 구분되는 함수 재정의는 불가능하다

int test(int A)
{
    return 0;
}

double test(int b)
{
    return 0.0;
}

위와 같이 parameter와 name이 같고 const가 없는 것이 똑같은 함수가
return type만으로 구분된다. 이러한 경우에는 컴파일 에러가 발생한다

Do not call virtual functions from constructors or destructors

우리는 virtual function을 constructor와 destructor에서 호출하면 안된다
즉 virtual 키워드를 constructor와 destructor에 사용하지 말라는 것이 아닌
constructor와 dsetructor에서 virtual function을 호출하지 말라는 말이다

Derived class가 생성되면 Base portion이 먼저 생성된다는 것을 기억하자
만약 이때 constructor에서 virtual function을 호출한다면 Derived 부분이 생성되기 전에 Base constructor가 실행되므로 virtual function을 호출해도 Derived 버젼은 찾을 수가 없어서 Base 버젼이 호출되는 것이다

Destructor 또한 마찬가지다 만약 Base 부분의 Destructor가 virtual function을 호출한다면 Base destructor가 실행될 때에는 이미 Derived가 파괴되고 난 이후이므로 Base 버젼이 호출될 것이다.

The downside of virtual functions

모든 함수를 virtual로 하면 좋겠지만 virtual function은 inefficient하다
자세한 내용은 다음에...

profile
청룡동거주민

0개의 댓글