Effective C++/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