생성자가 참으로 말썽 많은 존재인 것은, 클래스가 명시적인 생성자를 가지고 있지 않을 경우, 컴파일러가 암묵적인 생성자로 클래스를 생성하기 때문이다. 만약 클래스가 반드시 명시적인 초기화가 필요한 데이터 멤버들을 가지고 있을 경우, 이런 방식으로 클래스 생성하는 것은 파국을 초래할 가능성이 높다. 또, 데이터 멤버들이 명시적인 초기화가 필요하지 않더라도, 이것은 역시 좋은 코드는 아니다. 기본 자료형이 아닌 사용자 정의 자료형일 경우 어떤 오버헤드가 있을지 알 수 없고, 템플릿 자료형이라면 그야말로 예측 불가이다. 만약, 이 데이터 멤버들이 예외를 던진다면 이 예외를 받아낼 방법이 전혀 없다.
즉, 여기서 할 이야기는, '디폴트 생성자를 제외한, 반드시 필요한 생성자를 제공해야 하며, 가급적 디폴트 생성자가 호출되는 상황도 통제 아래에 두어라'이다. 특히, 이 클래스가 가지는 데이터 멤버들에 대한 안전한 예외 처리에 대한 내용이 핵심적이다. 다음 코드를 보자.
template< typename T >
class Test
{
private:
T m_data;
public:
Test();
};
이 클래스의 데이터 멤버인 m_data는 어떤 방식으로 초기화되어야 하는가? 특별히 초기화가 필요없는 데이터 멤버라면, T가 어떤 형식인지 알 수 없기 때문에 불안감은 있지만, 그냥 T 타입의 생성자가 호출되도록 두어도 문제는 없는 것 같다. 그리고, m_data가 어떤 방식으로 초기화가 필요하다면, 다음과 같은 생성자 정의가 흔히 사용된다.
Test()
{
m_data = initilization()..
}
그러나, 이 코드는 두 가지 심각한 문제를 가지고 있다. 첫 번째로, 생성 단계에서 T 타입에 대해 생성자와 대입 연산자가 호출된다. T 타입은 어떤 타입이라도 될 수 있지만, 이 코드를 사용하는 사람이 최소한 정의된 자료형만 사용한다고 하자. 그렇다고 하더라도 복사 생성 연산자나, 컴파일러에 따라서는 디폴트 생성자, 대입 연산자가 호출되기 때문에, 둘 중 어느 동작에서 막대한 비용을 사용해야 한다면 이것은 '모르는 사이에' 비효율적인 코드가 되어 버린다. 즉, T 타입이 어떤 방식으로 초기화되는지 알고 있거나, 그러한 형식을 제공한다면, 이것은 초기화 리스트를 사용하여 오버헤드 없이 한번의 동작으로 깔끔하게 처리하는게 훨씬 낫다.
그리고 두 번째로, 이것은 예외 처리가 되어 있지 않다. 위에서도 언급했지만, 생성, 대입 어느 단계에서라도 예외가 발생한다면 이 코드는 바로 붕괴된다. 즉, 암묵적인 생성자로 무사히 개체를 생성하더라도, 대입 단계에서 단순 복사가 적합하지 않거나 예외를 일으킬 가능성이 있다. 따라서, T 타입에 대한 이런 일련의 연산이 실행되는 동안, try - catch를 사용하여 예외를 잡아내야 한다.
이 이야기는 위의 초기화 리스트를 사용하여 최적화하라는 말과 상충되는 것으로 들릴지도 모르겠다. 초기화 리스트를 사용하면서, 어떻게 그 리스트에 대해 예외를 잡아내라는 말인가? 그러나, try - catch 구문은 이런 경우에 대해서도 사용할 수 있다. 다음과 같다.
Test() try : m_data(init..)
{
}
catch(...)
{
exception handling..
}
finally()
{
....
}
초기화 리스트를 사용한 데이터 멤버 초기화는 눈에 보이지 않는 불필요한 동작을 최소화 할 수 있는 좋은 방법이다. 또, 초기화 과정에서 발생할 수 있는 예외 역시 잡아낼 수 있으므로, 가급적 이 방법으로 데이터 멤버를 초기화하는 것이 좋다. 경우에 따라서는 이 방법이 필수적이기도 한데, 어떤 데이터 멤버는 디폴트 생성자를 제공하지 않기 때문이다. 정적인 자료형이 아니라 클래스일 경우 더욱 그렇다.
다만, 초기화 리스트를 사용할 경우 그 초기화 순서는 선언된 순서와 동일해야 하며, 상속된 클래스의 기반 클래스 생성자를 호출하는 것 또한 초기화 순서를 주의해야 한다. 기반 클래스의 생성자를 명시적으로 호출할 경우, 자신의 데이터 멤버보다 기반 클래스의 생성자가 먼저 호출되기 때문에 호출 순서에 주의해야 한다.
이 점을 주의하면, 보다 예외에 안전하고, 훨씬 효율적인 코드를 작성할 수 있을 것이다.
즉, 여기서 할 이야기는, '디폴트 생성자를 제외한, 반드시 필요한 생성자를 제공해야 하며, 가급적 디폴트 생성자가 호출되는 상황도 통제 아래에 두어라'이다. 특히, 이 클래스가 가지는 데이터 멤버들에 대한 안전한 예외 처리에 대한 내용이 핵심적이다. 다음 코드를 보자.
template< typename T >
class Test
{
private:
T m_data;
public:
Test();
};
이 클래스의 데이터 멤버인 m_data는 어떤 방식으로 초기화되어야 하는가? 특별히 초기화가 필요없는 데이터 멤버라면, T가 어떤 형식인지 알 수 없기 때문에 불안감은 있지만, 그냥 T 타입의 생성자가 호출되도록 두어도 문제는 없는 것 같다. 그리고, m_data가 어떤 방식으로 초기화가 필요하다면, 다음과 같은 생성자 정의가 흔히 사용된다.
Test()
{
m_data = initilization()..
}
그러나, 이 코드는 두 가지 심각한 문제를 가지고 있다. 첫 번째로, 생성 단계에서 T 타입에 대해 생성자와 대입 연산자가 호출된다. T 타입은 어떤 타입이라도 될 수 있지만, 이 코드를 사용하는 사람이 최소한 정의된 자료형만 사용한다고 하자. 그렇다고 하더라도 복사 생성 연산자나, 컴파일러에 따라서는 디폴트 생성자, 대입 연산자가 호출되기 때문에, 둘 중 어느 동작에서 막대한 비용을 사용해야 한다면 이것은 '모르는 사이에' 비효율적인 코드가 되어 버린다. 즉, T 타입이 어떤 방식으로 초기화되는지 알고 있거나, 그러한 형식을 제공한다면, 이것은 초기화 리스트를 사용하여 오버헤드 없이 한번의 동작으로 깔끔하게 처리하는게 훨씬 낫다.
그리고 두 번째로, 이것은 예외 처리가 되어 있지 않다. 위에서도 언급했지만, 생성, 대입 어느 단계에서라도 예외가 발생한다면 이 코드는 바로 붕괴된다. 즉, 암묵적인 생성자로 무사히 개체를 생성하더라도, 대입 단계에서 단순 복사가 적합하지 않거나 예외를 일으킬 가능성이 있다. 따라서, T 타입에 대한 이런 일련의 연산이 실행되는 동안, try - catch를 사용하여 예외를 잡아내야 한다.
이 이야기는 위의 초기화 리스트를 사용하여 최적화하라는 말과 상충되는 것으로 들릴지도 모르겠다. 초기화 리스트를 사용하면서, 어떻게 그 리스트에 대해 예외를 잡아내라는 말인가? 그러나, try - catch 구문은 이런 경우에 대해서도 사용할 수 있다. 다음과 같다.
Test() try : m_data(init..)
{
}
catch(...)
{
exception handling..
}
finally()
{
....
}
초기화 리스트를 사용한 데이터 멤버 초기화는 눈에 보이지 않는 불필요한 동작을 최소화 할 수 있는 좋은 방법이다. 또, 초기화 과정에서 발생할 수 있는 예외 역시 잡아낼 수 있으므로, 가급적 이 방법으로 데이터 멤버를 초기화하는 것이 좋다. 경우에 따라서는 이 방법이 필수적이기도 한데, 어떤 데이터 멤버는 디폴트 생성자를 제공하지 않기 때문이다. 정적인 자료형이 아니라 클래스일 경우 더욱 그렇다.
다만, 초기화 리스트를 사용할 경우 그 초기화 순서는 선언된 순서와 동일해야 하며, 상속된 클래스의 기반 클래스 생성자를 호출하는 것 또한 초기화 순서를 주의해야 한다. 기반 클래스의 생성자를 명시적으로 호출할 경우, 자신의 데이터 멤버보다 기반 클래스의 생성자가 먼저 호출되기 때문에 호출 순서에 주의해야 한다.
이 점을 주의하면, 보다 예외에 안전하고, 훨씬 효율적인 코드를 작성할 수 있을 것이다.