생성자, 소멸자, 대입 연산자(2)
예외가 소멸자를 떠나지 못하도록 소멸자로부터 예외가 터져 나가는 경우를 C++ 언어에서 막는 것은 아니지만, 실제 상황을 들춰보면 확실히 막을 수밖에 없다.
vector 타입의 객체 v, 다시 말해 벡터 v가 소멸될 때, 자신이 거느리고 있는 Widget들 전부를 소멸시킬 책임은 벡터에게 있다. v에 들에 있는 Widget이 열 개인데, 첫 번째 것을 소멸시키는 소멸시키는 도중에 예외가 발생되었다고 가정할 때, 나머지 아홉 개는 여전히 소멸되어야 하므로 v가 소멸자를 호출해야 한다. 그런데 이 과정에서 문제가 또 터졌다고 가정한다면, 두 번째 Widget에 대해 호출된 소멸자에서 예외가 던져진다면 어떻게 될까? 현재 활성화된 예외가 동시에 두 개나 만들어진 상태, C++의 입장에서는 감당하기에 버겁다. 이 두 예외가 동시에 발생한 조건이 어떤 미묘한 조건이냐에 따라 프로그램 실행이 종료되든지 아니면 정의되지 않은 동작을 보이게 될 텐데, 이 경우에는 프로그램이 정의되지 않은 동작을 보인다. 다른 표준 라이브러리 컨테이너를 쓰더라도 결과는 마찬가지이며, 심지어 배열을 써도 마찬가지, 컨테이너나 배열을 썼기 때문에 생긴 일이 아니다. 완전하지 못한 프로그램 종료나 미정의 동작의 원인은 예외가 터져 나오는 것을 내버려 두는 소멸자에게 있다.
소멸자는 예외가 발생하면 프로그램을 바로 끝냅니다. 대개 abort를 호출합니다.
객체 소멸이 진행되다가 에러가 발생한 후에 프로그램 실행을 계속할 수 없는 상황이라면 좋은 선택, 소멸자에서 생긴 예외를 그대로 흘려 내보냈다가 정의되지 않은 동작에까지 이를 수 있다면, 불상사를 막는다는 의미에서 어느 정도 장점도 있다.
예외를 삼켜버린다.
대부분의 경우에서 예외 삼키기는 그리 좋지 않습니다. 중요한 정보가 묻혀 버리기 때문, 무엇이 잘못됐는지를 알려 주는 정보, 때에 따라서는 불완전한 프로그램 종료 혹은 미정의 동작으로 인해 입는 위험을 간수하는 것보다 그냥 예외를 먹어버리는 게 나을 수도 있다. 단, 예외 삼키기를 선택한 것이 제대로 빛을 보려면, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 한다.
어느 쪽을 택하든 특별히 좋을 건 없어 보인다. 둘 다 문제점이 있기 때문, 중요한 것은 close가 최초로 예외를 던지게 된 요인에 대해 프로그램이 어떤 조치를 취할 수 있는가인데, 이런 부분에 대한 대책이 전무한 상태
하지만 사실 어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다는 것이 포인트, 예외를 일으키는 소멸자는 시한폭탄이나 마찬가지라서 프로그램의 불완전 종료 혹은 미정의 동작의 위험을 내포하고 있기 때문,
* 소멸자에서는 예외가 빠져나가면 안 된다. 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 한다.
* 어떤 클래스의 연산이 진행되거나 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(소멸자가 아닌 함수)이어야 한다.
객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자
호출 결과가 원하는 대로 돌아가지 않을 것
기본 클래스의 생성자가 호출될 동안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다.
그 대신, 객체 자신이 기본 클래스 타입인 것처럼 동작.
기본 클래스 생성자는 파생 클래스 생성자보다 앞서서 실행되기 때문에, 기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 생성자보다 앞서서 실행되기 때문에, 기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니라는 것이 핵심, 기본 클래스 생성자에서 어쩌다 호출된 가상 함수가 파생 클래스 쪽으로 내려간다면 파생 클래스 버전의 가상 함수는 파생 클래스만의 데이터 멤버를 건드릴 것이 뻔한데 이들은 아직 초기화되지 않았다. 이렇듯 어떤 객체의 초기화되지 않은 영역을 건드리는 것은 치명적인 위험을 내포합니다.
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스, 호출되는 가상 함수는 모두 기본 클래스의 것으로 결정될 뿐만 아니라, 런타임 타입 정보를 사용하는 언어 요소(dynamic_cast 혹은 typeid 같은 것)를 사용한다고 해도 이 순간엔 모두 기본 클래스 타입의 객체로 취급, 객체의 기본 클래스 부분을 초기하기 위해 생성자가 실행되고 있는 동안에는, 그 객체의 타입이 기본 클래스라는 뜻, 이런 식의 처리는 C++ 언어의 다른 모든 기능에서 이루어지고 있고, 타당성도 충분, 클래스만의 데이터는 아직 초기화된 상태가 아니기 때문에, 아예 없던 것으로 취급하는 편이 최고로 안전, 파생 클래스의 생성자의 실행이 시작되어야만 그 객체가 비로소 파생 클래스 객체의 면모를 갖게 된다.
객체가 소멸될 때는 파생 클래스의 소멸자가 일단 호출되고 나면 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정하기 때문에, C++은 이들을 없는 것처럼 취급하고 진행, 기본 클래스 소멸자에 진입할 당시의 객체는 더도 덜도 아닌 기본 클래스 객체가 되며, 모든 C++ 기능들(가상 함수) 역시 기본 클래스 객체의 자격으로 처리
생성자 혹은 소멸자 안에서 가상 함수가 호출되는지를 잡아내는 일이 항상 쉬운 것은 아니다.
생성자가 여러 개가 될 때, 각 생성자에서 하는 일이 조금씩은 다르겠지만 똑같은 작업을 모아 공동의 초기화 코드로 만들어 두면 좋지만 이 안에서 가상 함수를 호출한다고 가정했을 때, 컴파일도 잘 되고 링크도 말끔하게 된다. 하지만, 분명하게 문제가 생긴다. 이런 문제를 피하는 방법은 다른 게 없다. 생성 중이거나 소멸 중인 객체에 대해 생성자나 소멸자에서 가상 함수를 호출하는 코드를 철저히 솎아내고, 생성자와 소멸자가 호출하는 모든 함수들이 똑같은 제약을 따르도록 만드는 일 외에는 없다.
기본 클래스 부분이 생성될 때는 가상 함수를 호출한다 해도 기본 클래스의 울타리를 넘어 내려갈 수 없기 때문에, 필요한 초기화 정보를 파생 클래스 쪽에서 기본 클래스 생성자로 올려주도록 만듦으로써 부족한 부분을 역으로 채울 수 있다.
미초기화된 데이터 멤버는 정의되지 않은 상태에 있다는 게 중요하다.
이것 때문에 기본 클래스 부분의 생성과 소멸이 진행되는 동안에 호출되는 가상 함수가 무턱대고 파생 클래스 쪽으로 내려가지 않는 것
* 생성자 혹은 소멸자 안에서 가상 함수 호출 X, 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않기 때문이다