클래스의 특징은?
이런 시험 문제가 나오면 다형성을 비롯한 여러 대답이 나올 수 있지만, 뭐니뭐니 해도 상속이야말로 클래스의 백미가 아닌가 싶다. C++ 클래스 다형성의 상당 부분은 상속으로(나머지는 템플릿으로) 구현되기 때문이다.
한데, 클래스라고 무턱대고 상속 받으면 곤란하다. STL 컨테이너처럼 멋진 클래스를 보면 당장에 상속 받아서 그 기능을 맘껏 뽐내고 싶겠지만, 세상에는 상속세란 게 있다.
C++ 클래스를 상속 받으려면 먼저 부모님이 상속해줄 마음이 있는지부터 확인해야 한다. 달리 말해 소멸자가 virtual 로 선언되어 있는지 봐야 한다. 특히 STL이 제공하는 클래스는 가상 소멸자가 없다고 봐도 된다. 무턱대고 이를 상속 받으면 참 난감하다.
허나 여태까지 이렇게 잘 살아온 사람이 많을 게다. 그도 그럴 것이 가상 소멸자가 없어도 파생 클래스의 기능이 제대로 작동하는 듯 보이기 때문이다. vector를 상속해서 이런저런 테스트를 해보면 아무런 이상이 없는 것 같다. push_back도 잘 되고 iterator도 문제 없다. 그렇다면 도대체 뭐가 문제인가?
Effective C++ 중 항목 7은 ‘다형성을 가진 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자’라며 다음과 같이 설명한다.
C++ 규정에 의하면, 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때 그 기본 클래스에 비가상 소멸자가 들어 있으면 프로그램 동작은 미정의 사항이라고 되어 있습니다.
이를 검증해보려고 단위 테스트 코드를 짜봤다.
class MapMock : public map<String,String> { public: typedef xmap<String, String>::iterator Iterator; typedef xmap<String, String>::const_iterator ConstIterator; typedef pair<String, String> Pair; static BOOL m_Deleted; }; BOOL MapMock::m_Deleted = FALSE; class MapMock2 : public MapMock { public: explicit MapMock2() { m_Deleted = FALSE; } ~MapMock2() { m_Deleted = TRUE; } }; SUITE(Trivial) { TEST(MapInheritance) { MapMock* mock = new MapMock2(); CHECK(mock->m_Deleted == FALSE); delete mock; CHECK(mock->m_Deleted == TRUE); //! 여전히 FALSE인 게 문제다. } }
참고 문헌
첨언하자면, 당장 문제가 되지 않는다고 잠재적인 버그를 남겨 놓는 사람들은 반성해야 한다. 예를 들어 vector를 상속 받은 myvector가 있더라도 vector의 포인터로 myvector 인스턴스를 지우지 않으면 큰 문제가 되지 않는다. 그래서 문제가 없다고 말하는 일이 잦다. 실용주의 프로그래머 같은 책을 읽을 때는 방어적 프로그래밍이 중요하다는 주장에 공감하던 사람도 막상 자기 일이 되면 어영부영 넘어가려고 한다. 당장 문제 없다고 넘어가면 이런 문제가 있었다는 사실조차 잊어먹었을 때 문제가 다시 나타나 당신의 발목을 잡게 된다. 그 무렵엔 똑같은 문제가 예전에 발생했었단 사실조차 잊었을 테니 자기 반성할 기회도 없을 것이다.
오호라.. 가상소멸자가 클래스에서 동적으로 자원을 할당받은 것이 있을 때만 필요한 줄 알고, 후루룹 할아버지의 책에서 다형성을 가지는 클래스에서는 가상소멸자를 만들어야 한다기에 왜그럴까 했는데, 이제 이해되었습니다. 님의 블로그는 RSS리더에 등록하고 계속 보고 있는데, 처음 댓글 남깁니다.
처음 뵙겠습니다.
최근에 이 문제 때문에 핸들 누수가 발생한 적이 있어서, 이렇게 글을 써봤습니다. 아, 그러고 보니 클래스 작성할 때 이와 관련된 몇 가지 원칙이 있는데, 댓글로는 그렇고 또 글 하나 써야겠네요. ㅎㅎ
상속세 무지 재밌는 표현입니다 ^^; 말씀대로 virtual destructor가 없으면 메모리 누수가 발생하죠. 얼마전 전화 인터뷰에서 받은 문제이기도 하군요 ㅎㅎ
가상 소멸자의 이유에 대해서 상당히 회의적이었는데… 상위 클래스의 포인터로 하위 객체를 지우게 되면 문제가 생기는군요. ㅎㅎ
좋은 글 잘 읽고 갑니다. ^^
제가 참고 문헌으로 적어놓은 글을 읽어보시면 좀더 재미있는 사례가 있습니다. stl 컨테이너를 부분 특화시켜서 쓰고 싶을 때가 있는데, 이때 vector 등을 상속하고 싶은 욕구가 일거든요. 하지만 virtual 소멸자가 없으니 상속 받자니 꺼림칙합니다. 그렇다고 부분 특화를 간단히 할 수 있느냐 하면 그것도 아니거든요. 참고 문헌에 제시된 새 방법이 어서 지원됐으면 하는 바램입니다. 좀 귀찮거든요.
이것이 잠재적인 문제라고 생각을 하지 못하는 것도 문제겠지요.. 저도 상속세라는 표현에 아주 신나게 웃었습니다.. ㅋㅋㅋ
새해 복 많이 받으세요~
코드마다 나름의 역사가 있다 보니, 처음엔 상속 관계가 없었거나 별 문제가 안 되었겠죠. 저야 나중에 고치는 입장이다 보니 까칠한 어조가 되긴 했지만요. ㅎㅎ
새해 복 많이 받으세요~~~~~
레몬 펜 좀 설치해 주세요. 줄 좀 긋게..
크, 요즘 유행인가 보군요. 퇴근하고 집에 가서 알아보겠습니다.
레몬펜 설치했습니다. 부랴부랴 설치해놔서 어떤 기능이 있는지 아직 모르겠네요. 시간날 때 자세히 들여다봐야겠습니다.
CoMK!D의 생각
왜 STL container를 상속 받으면 안된다고 알고 있었지.. -_-a 주의해서 써야 한다는 걸 쓰면 안 된다로 인식해버렸던 건가..