C++11 기능 소개: 상속생성자(Inheriting Constructor)

Posted at 2012. 3. 28. 22:51 // in S/W개발/C++11 // by 김윤수


상속생성자란 기본클래스에 정의된 생성자를 상속받는 클래스에서 상속받을 수 있도록 하는 기능이다.

상속생성자는 컴파일러가 기본생성자와 복사생성자를 생성하는 규칙 때문에 발생하는 문제점을 해결하기 위한 기능이므로 우선 이 규칙을 이해할 필요가 있다. C++ 컴파일러는 프로그래머가 생성자를 정의하지 않으면 기본 생성자 및 복사생성자를 알아서 생성한다. 예를 들어,

class B {
 int v_;

public:
 int get();
 void set(int v);
};

B b;

와 같은 코드가 아무런 문제 없이 컴파일되는 이유는 컴파일러가 기본생성자를 생성해 주기 때문이다. 그런데 B를 쓰다 보면 기본생성자에 의해 초기화되지 않은 멤버 변수 값 때문에 문제가 생기는 경우가 있기 마련이다. 예를 들어,

void f();

B b;
f(b.get());

와 같은 코드를 실행할 경우 b.v_에 담긴 쓰레기 값 때문에 문제가 발생할 수 있을 것이다. 이런 문제를 발견했을 때 상식있는 프로그래머라면 다음과 같이 생성자를 추가할 것이다.

class B {
 int v_;

public:
 B(int v) : v_(v) {}
 int get();
 void set(int v);
};

B b;

그러면 당장 다음과 같은 에러가 발생하게 된다.

error C2512: 'B' : 사용할 수 있는 적절한 기본 생성자가 없습니다.

그렇다. 프로그래머가 기본생성자가 아닌 생성자를 하나라도 추가하면 컴파일러가 기본생성자를 생성하지 않는 것이다. 이제 프로그래머가 직접 기본생성자를 작성해야 한다.

class B {
 int v_;

public:
 B() : B(0) {} // C++11의 위임생성자 기능 활용
 B(int v) : v_(v) {}
 int get();
 void set(int v);
};

그런데 만약 B로부터 상속 받은 D라는 클래스가 있었다고 생각해 보자.

class D : public B {
public:
 void compute();
};

B::B(int)가 추가된 이유를 이해하는 프로그래머라면 D를 사용할 때 다음과 같이 생성시 초기값을 제대로 지정하려고 할 것이다.

D d(10);

그렇지만 위 코드는 컴파일 에러를 발생시킨다. 왜냐하면 D::D(int)는 애초에 정의되어 있지 않기 때문이다. 결국 D를 작성한 프로그래머는 다음과 같이 D::D(int)를 추가해야 한다.

class D : public B {
public:
 D(int v) : B(v) {}
 void compute();
};

D d(10);

그렇다면 다음 코드는 어떨까?

D d2;
d2.set(10);

D::D(int)가 없었기 때문에 충분히 여러 차례 사용될만한 코드 패턴일 것이다. 그렇지만 D를 작성한 프로그래머가 D::D(int)를 추가한 순간 위와 같은 코드 패턴들은 모두 컴파일 에러를 일으키게 된다. D 클래스를 작성한 프로그래머와 위 코드를 작성한 프로그래머가 같다면 별 문제 없겠지만 다르다면 사무실 어느 구석에선가 “악~”하고 소리가 날 일이다. 결국 D는 다음과 같이 작성되어야 한다.

class D : public B {
public:
 D() : B() {}
 D(int v) : B(v) {}
 void compute();
};

D d(10);
D d2;
d2.set(10);

창조적 귀차니즘이 있는 프로그래머라면 이쯤에서 의문이 하나 생긴다. “왜 이렇게 불편하게 해 놓았지? 애초에 DB의 생성자들을 다 물려받으면 되잖아. 상속에는 생성자 상속까지 포함되어야 하는 거 아닌가?”

상속생성자는 이런 의문에서 출발한 기능이다. 위 질문에 대한 해답으로 상속시에 생성자를 모두 물려받는 것으로 정하면 되지 않을까 하는 생각이 제일 먼저 들 것이다. 그렇지만 이렇게 상속의 의미를 수정할 경우 기존에 이미 존재하던 수많은 코드들이 컴파일되지 않거나 컴파일은 되는데 예상치 못한 실행시 에러가 발생할 수 있다. 언어가 발전함에 있어 이런 급작스런 변화는 거의 불가능하다고 볼 수 있다. 기존 코드를 모두 깨뜨려버린다면 누구도 새로운 기능들을 채택하려 하지 않을 것이기 때문이다.

그렇다면 결국 생성자를 따로 상속받을 수 있는 방법을 고안해 내야 할 것이다. C++11 표준화 논의 중에 여러가지 방법들이 제안되었지만 최종적으로는 다음과 같이 using 선언을 이용하여  생성자를 물려받을 수 있다.

class B {
 int v_;

public:
 B() : B(0) {} // C++11의 위임생성자 기능 활용
 B(int v) : v_(v) {}
 int get();
 void set(int v);
};

class D : public B {
public:
 using B::B;
 void compute();
};

위와 같이 정의할 경우, D::D(int)가 암묵적으로 정의되며, 그 의미는 다음과 동일하다고 생각하면 된다.

class D : public B {
public:
 D(int v) : B(v) {}
 ...
};

따라서 다음과 같이 D를 사용할 수 있게 된다.

D d1; // 암묵적으로 정의됨
D d2(10); // 상속을 통해 암묵적으로 정의됨
D d3(d2); // 암묵적으로 정의됨

D::D(int)가 “암묵적으로” 정의된다는 것은 기본생성자인 D::D()과 복사생성자인 D::D(const D&)가 암묵적으로 정의된다는 의미이다. 왜냐하면 D::D(int)를 프로그래머가 명시적으로 정의한 것은 아니기 때문에 기본생성자 및 복사생성자 생성규칙에 따라 암묵적으로 정의된 것이다.

이렇게 상속생성자가 동일한 signature를 갖는 생성자를 반복해서 작성해야 하는 수고로움은 덜어 줬지만 잘못하면 다른 문제를 일으킬 수도 있다. 다음 예제를 보자.

class B {
 int v_;

public:
 B() : B(0) {} // C++11의 위임생성자 기능 활용
 B(int v) : v_(v) {}
 int get();
 void set(int v);
};

class D : public B {
 float v2_; // 초기화 필요
public:
 using B::B;
 void compute();
};

D d3(25);

언뜻 보기에는 문제가 없어 보일 수 있으나 컴파일러가 생성한 D::D(int)B::B(int)를 뜻하므로 d3.v2_는 초기화되지 않은 채로 사용될 것이다. 이쯤에서 우리는 다음과 같은 프로그래밍 경구를 기억할 수 밖에 없다.

“초기화되지 않은 변수를 사용하지 말라”

구식 방법으로 D::D(int)를 직접 작성하여 초기화할 수도 있지만, C++11에 새롭게 추가된 멤버초기화 기능을 이용하여 더 멋지게 해결할 수도 있다.

class D : public B {
 float v2_{0.0};        // 멤버초기화 기능 이용
public:
 using B::B;
 void compute();
};

D d3(25);                // v2_는 0.0으로 초기화 됨

참고문헌
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1890.pdf
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2540.htm
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1898.pdf
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1583.pdf
http://www2.research.att.com/~bs/C++0xFAQ.html#inheriting
http://occamsrazr.net/tt/205