스토리텔링 개발자

[More Effective C++] 25. 함수를 가상 함수처럼 만들기 본문

개발/More Effective C++

[More Effective C++] 25. 함수를 가상 함수처럼 만들기

김디트 2024. 9. 4. 11:05
728x90

항목 25. 생성자 함수와 비멤버 함수를 가상 함수처럼 만드는 방법

 

 

 

가상 생성자(virtual constructor) : 생성자 함수를 가상 함수처럼
  • 런타임에 타입을 체크하겠다(가상) + 명확한 타입의 객체에 대한 메모리 할당(생성자) 가 어떻게 양립 가능할까?
  • 하지만 가능한 일이다. 게다가 비일비재하게 사용한다.
class NLComponent // 추상 기본 클래스
{
public:
    ... // 순수 가상 함수가 최소한 하나는 있다.
};

// 순수 가상 함수가 없는 하위 클래스들
class TextBlock : public NLComponent { ... };
class Graphic : public NLComponent { ... };


class NewsLetter // 뉴스레터
{
public:
    NewsLetter(istream& str); // 디스크 데이터를 읽기 위한 istream
    ...
private:
    // 디스크 데이터를 NLComponent로 할당해주는 함수
    // 이 함수는 생성자처럼 동작하며,
    // 런타임에 디스크에서 읽은 정보에 따라 다른 타입의 객체를 만든다.
    // 즉 이 함수를 가상 생성자(virtual constructor)라 부를 수 있다.
    static NLComponent* readComponent(istream& str);

private:
    // NLComponent 객체를 리스트로 관리한다.
    list<NLComponent*> component;
};

NewsLetter::NewsLetter(istream& str)
{
    while(str)
    {
        // 객체를 생성하여 뉴스레터 구성요소 리스트에 추가
        components.push_back(readComponent(str));
    }
}
  • 가상 복사 생성자(virtual copy constructor)
    • 가상 생성자 중 특히 널리 쓰이는 함수
    • 함수를 호출한 객체를 그대로 본뜬 사본의 포인터를 반환한다.
    • 복사 생성자를 그대로 이용하여 구현하기 때문에, 유지보수에도 편리하다.
    • 가상 함수에 대한 완화된 규칙이 사용된다.
      • 기본 클래스의 가상 함수를 재정의한 파생 클래스의 가상 함수는 똑같은 반환 타입이 아니어도 된다.
  • 가상 복사 생성자를 사용한 뉴스레터
class NLComponent
{
public:
    NewsLetter(const NewsLetter& rhs);

    // 가상 복사 생성자
    // 해당 클래스의 진짜 복사 생성자를 호출해주는 것 뿐이지만
    // 가상 함수(clone)를 통해 복사를 진행한다.
    virtual NLComponent* clone() const = 0;
    ...
};

class TextBlock : public NLComponent
{
public:
    // 반환값이 기본 클래스 버전과 다르다.
    virtual TextBlock* clone() const
    {
        return new TextBlock(*this);
    }
    ...
};

class Graphic : public NLComponent
{
public:
    virtual Graphic* clone() const
    {
        return new Graphic(*this);
    }
    ...
};

NewsLetter::NewsLetter(const NewsLetter& rhs)
{
    for(list<NLComponent*>::const_iterator it = rhs.components.begin() ;
        it != rhs.components.end() ; ++it)
    {
        components.push_back((*it)->clone());
    }
}

 

 

 

비멤버 함수를 가상 함수처럼
  • 멤버 함수를 가상 함수로 선언하여 처리하기 힘든 경우가 있다.
  • 예컨대 아래의 경우, 가독성에 심각한 문제가 생긴다.
class NLComponent
{
public:
    // 출력 연산자 재구현을 위한 순수 가상 함수화
    // 하지만...
    virtual ostream& operator<<(ostream& str) const = 0;
    ...
};

class TextBlock : public NLComponent
{
public:
    virtual ostream& operator<<(ostream& str) const;
};

class Graphic : public NLComponent
{
public:
    virtual ostream& operator<<(ostream& str) const;
};

TextBlock t;
Graphic g;

// 다음 코드는 어떻게 봐도 cout으로 출력하는 것으로 보이지 않는다..
t << cout;
g << cout;
  • 출력 연산자를 대신해 커스텀 가상 함수로 구현할 수는 있겠지만..
class NLComponent
{
public:
    virtual ostream& print(ostream& s) const = 0;
    ...
}

class TextBlock : public NLComponent
{
public:
    virtual ostream& print(ostream& s) const;
    ...
};

class Graphic : public NLComponent
{
public:
    virtual ostream& print(ostream& s) const;
    ...
};

TextBlock t;
Graphic g;

// 일단 보기엔 나아졌지만, 다른 객체들과의 통일성이 깨진다.
t.print(cout);
g.print(cout);
  • 다른 타입에 대해서는 operator<<를 사용하는데 NLComponent 하위 객체만 print를 사용한다?
    • 썩 만족스러운 결론은 아니다.
  • 이 경우 비멤버 함수로 operator<<를 빼내고, 내부 구현은 print 함수를 재활용하면 모든 것이 만족스러워진다.
// operator<< -> print 로 함수가 두 번 호출되는 비용이 싫으므로 인라인 선언
inline ostream& operator<<(ostream& s, const NLComponent& c)
{
    return c.print(s);
}

TextBlock t;
Graphic g;

// 이제 기대하던 바대로 사용할 수 있게 되었다.
cout << t;
cout << g;

 

 

 

번외
  • 두 개 이상의 인자에 대해서도 가상 함수처럼 동작하게 할 수 있지 않을까?(항목 31 참조)
728x90
Comments