2009. 7. 25. 10:48

타입변환 함수에 대한 주의를 놓지 말자!

+. 컴파일러가 사용하는 타입변환 함수

  • 단일 인자 생성자 (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++