- 단일 인자 생성자 (single-argument constructor)
- 암시적 타입변환 연산자 (implicit type conversion operator)
+. 타입변환 연산자의 함정
class Rational { public: ... operator
double() const; // Rational 을double 로암시적으로변경 }; |
와 같이 double 에 대한 타입 변환 연산자가 정의 되어 있다. 다음을 살펴보자.
Rational r(1, 2); cout << r; // "1/2"를출력해야한다. |
위 코드는 "1/2"를 출력하거나, 만약 operator << 가 정의되어 있지 않다면 컴파일 에러가 나야 할 것이다. 하지만 Rational 객체를 받는 operator<< 를 호출해야 하는 시점에서 이 함수를 찾지 못하지만, 어떻게든 함수 호출을 성공시키기 위해 암시적인 타입변환 함수를 찾아 맞추어 본다. 이 경우에는 Rational::operator double() 을 찾아내어 암시적인 타입변환을 수행하고, 결국 operator << 함수 호출은 double 타입이 출력되는 의도하지 않은 상황이 발생한다.
이러한 문제를 해결하기 위해서는 이 연산자를 아래와 같이 멤버 함수로 정의하는 것이다.
class Rational { public: ... double ToDouble() const; // Double 타입을반환함. }; |
그리곤 이 멤버 함수를 직적 호출한다.
Rational r(1, 2); cout << r; // 에러! cout << r.ToDouble(); // 반환된double 값을출력한다. |
이때문에, 보통 C++ 프로그래머들은 연륜이 쌓일수록 타입변환 연산자를 꺼린다. C++ 표준 라이브러리를 제정하고 있는 위원회 임원들을 예로 들어도 그렇다. 업계 최고의 전문가인 그 사람들이 만든 string 클래스에는 string 타입을 C 스타일의 char* 로 바꿔주는 암시적 변환 연산자가 없다. 대신에 직접 호출할 수 있는 c_str 을 두고 이것으로 타입을 변환하도록 한다. 이것이 우연일까?
+. 단일 인자 생성자를 통한 암시적 타입변환
단일 인자 생성자때문에 일어나는 문제는 암시적 타입변환 연산자 때문에 발생하는 문제보다 많은 경우에 더 고약하다.
template <class T> class Array{ public: Array(int lowBound, int highBound); Array(int size); // 단일인자생성자 T
& operator[] (int
index); ... }; |
에 대하여 다음의 경우를 살펴보자.
bool operator==(const Array<int> & lhs, const Array<int>& rhs); Array<int> a(10); Array<int> b(10); ... for(int i=0;
i<10; i++) { if(a == b[i]) { // a 는a[i] 가되어야하지만오타 ... } else { ... } } |
어쩌다가 실수로 a 옆에 배열 인덱스 연산자를 붙이지 않았다. 에러가 나야 하지만 int 하나만 받아들이는 단일 인자 생성자 때문에 암시적 타입변환이 수행되어 다음과 비슷한 코드가 생긴다.
for(int i=0;
i<10; i++) { if( a == static_cast<Array<int>
>(b[i])
) { ... } } |
이것이 의도한 동작일 가능성은 없을 것이다. 게다가 이 코드는 짜증날 정도로 비효율적이다. 루프가 끝날때까지 매번 임시 Array<int> 객체를 만들었다가 없애기 때문이다.
이러한 단일인자 생성자때문에 일어나는 암시적 타입변환을 막는 방법에는 두가지가 있다.
하나는 explicit 키워드를 사용하는 쉬운 방법이고, 두 번째 방법은 explicit 를 지원하지 않는 컴파일러의 경우에 proxy class 를 사용하는 방법등이 있다.
+. explicit 키워드 사용
template <class T> class Array{ public: ... explicit
Array(int size); // 매개변수와호출이명확할때에만호출하라. // 암시적타입변환에사용되지않는다. ... }; |
explicit 키워드는 암시적 타입변환의 문제를 막기 위해 만들어진 것이다. 매개변수와 호출이 명확할 때에만 이 생성자를 호출하라고 알려주는 뜻이므로, explicit 로 선언된 생성자는 암시적 타입변환에 사용되지 않는다.
+. Proxy Class 사용
template <class T> class Array{ public: class ArraySize { // 새로만든Proxy Class public: ArraySize(int numElements) :
theSize(numElements)
{ } int
size() const
{ return theSize;
} private: int
theSize; }; Array(int lowBound, int highBound); Array(ArraySize size); // 선언형식이바뀌었다. ... }; |
ArraySize 를 Array의 내부 클래스로 만든이유는 이 클래스가 Array 하고만 사용된다는 사실을 강조하기 위해서이다.
아래 문에서 어떤 일이 일어날지 생각해 보자.
Array<int> a(10); // 인자는정수이다. |
컴파일러는 int 를 받아들이는 Array<int> 클래스의 생성자를 호출하려 하지만 이런 생성자는 없다. 궁지에 몰린 컴파일러는 미친 듯이 클래스 선언부를 찾다가 '옳거니' 하면서 int 인자를 임시 ArraySize 객체로 바꿀 수 있다는 사실을 발견한다. ArraySize 객체는 Array<int> 생성자가 원하던 바로 그것이기 때문에, 컴파일러는 부담 없이 타입변환을 수행한다. 결국 생성자 호출은 성공이다.
다음과 비슷한 코드로 호출될 것이다.
Array<int> a(static_cast<ArraySize>(10)); |
앞에 보았던 코드를 다시 보도록 하자.
bool operator==(const Array<int> & lhs, const Array<int>& rhs); Array<int> a(10); Array<int> b(10); ... for(int i=0;
i<10; i++) { if(a == b[i]) { // a 는a[i] 가되어야하지만오타 |
컴파일러는 Array<int> 객체에 대해 호출할 operator == 연산자의 우변으로 Array<int> 타입이 왔으면 했지만, int 객체를 받는 생성자가 이제는 없다. 게다가, 컴파일러는 int 를 임시 ArraySize 객체로 바꾼 다음에 그 객체로부터 다시 Array<int> 객체를 생성하는 고단수의 처리는 생각할 줄 모른다.
ArraySize 처럼 쓰이는 클래스를 가리켜 프록시 클래스(proxy class)라고 하는데, 다른 객체를 대신한다고 해서 붙은 이름이다.
여기까지의 내용을 요약하면, 결국 의도하지 않은 상황을 초래할 수 있는 암시적 타입변환을 사전에 예방하기 위해 다음과 같이 구현해야 한다는 내용이다. 1. 되도록이면 타입변환 연산자 대신 특정 타입을 반환하는 멤버 함수를 정의한다. 2. 단일 인자 생성자에는 explicit 를 선언하여 암시적 타입변환을 막는다. (구 컴파일러의 경우 proxy class 사용) 이와 같이 정리하면 이해가 빠를 것이다. |
참고 : More Effective C++