반응형

출처 : http://blog.naver.com/kzh8055/140062582343

 

 


[ Effective C++ ] 암시적 타입 변환( Implicit Type Casting )  Effective !!! 2009/02/06 21:51

 

 
흠...

 

이번 글은 암시적인 타입 변환에 대해서다.

사실 EC++ 에 '암시적인 타입변환'이란 항목 자체가 있는건 아니지만 책 전반에 걸쳐

계속 언급돼는 중요한 내용이고 간혹 헷갈리는 부분도 있고해서 정리하고 넘어가야겄다.

 

암시적 타입 변환 VS 명시적 타입 변환

 

암시적( Implicit ) 과 반대돼는 개념은 명시적( Explicit ) 일것이다.

명시적은 말그대로 어떤 의도를 대놓고(?) 나타낸다는 의미이고

그렇다면 명시적 타입변환( Explicit Type Casting )이란

어떤 타입을 또 다른 타입으로 바꾸는데 있어 사용자가 직접 그 타입 변환을 수행하는

캐스팅 명령어를 코딩하는 경우라 볼수 있다.

 

비교적 타입 제한에 관대한 C 언어에서는 명시적 캐스팅 방법 달랑 하나로

거의 대부분의 경우 이방법을 사용해 모든 타입으로의 캐스팅이 가능하다.

- 심지어는 호환돼는 타입이 아닐경우라도( 실수형에서 문자형으로의 타입 변환등 ) 가능하다 ㅡ,.ㅡ

 

float radian= 3.14

int a = ( int )radian;              // C 스타일의 ( 명시적인 )캐스팅< float 에서 int 로 >

 

그럼 C++ 에서 제공하는 타입 변환자 는 어떠한가?

타입 검사에 있어선 한층 까탈스러워진 C++ 의 경우 각각 용도에 맞는 타입변환자가

4 개 씩이나 준비 돼있다. 그게 바로 static_cast, const_cast, dynamic_cast, reinterpret_cast 이다.

- 이것에 대한 자세한 설명은 책이나 웹 상에 널려있는 정보를 활용해라

 

하여간 여기서 중요한 부분은 명시적 타입 변환은 사용자( 프로그래머 )의

타입변환 의도가 직접 코드에 반영( 타입 변환자 등 으로 )돼 있다는 것이다.

- 쉽게 말해 캐스팅 연산을 적용했다는거다.

 

그러면 이와는 반대인 암시적인 타입 변환은 뭔가?

위의 맥락 상으로는 타입 변환자 등 타입을 캐스팅하기 위한 부분이 코드상에

( 명시적으로 ) 반영돼지 않았음에도 불구 하고 자동으로 타입 변환이 이뤄지는것을 말할것이다.

 

사용자가 작성한 코드에는 타입 변환 의도가 없었음에도 자동으로 타입변환이 발생한다면

과연 이것은 누구의 소행일것인가?

 

범인( ? )은 바로 컴파일러다. 컴파일러는 일련의 규칙에 따라( 컴파일러 제작자가 의도한 )

상황에 맞다고 생각하면 기특하게도 사용자가 구지 시키지도 않은 타입변환을

지가 스스로 수행하는 것이다.

 

흔히 발생하는 암시적인 타입 변환의 예를 들어 봅시다.

 

class Base{ ... };                          // Base 클래스

class Derived : public Base{ ... };   // Base 로 부터 상속 받은 Derived 클래스

 

...

/* Client Code */

 

Base* pBase = new Base;     // 오케바리! 동일한 타입의 객체를 동일한 타입의 포인터로 가르킴

pBase = new Derived;            // 이 또한 이상 무! 자식 객체는 부모 객체의 포인터로 가리킬수 있다

 

C++ 을 비롯한 객체 지향언어를 사용해본 인간이라면

 

[ 부모 타입 포인터 ] = [ 자식 타입 객체( 주소 ) ];

 

와 같은 형태의 코딩을 수없이 해왔을것이다.

사실 따지고 보면 엄연히 두 타입이 서로 같지 않은 셈이니

C++ 컴파일러가 왠지 타입 불일치 에러를 뱉어낼 듯 하나

희안하게 아무련 불평없이 깔끔히 컴파일 된다.

 

위 코드가 전혀 문제 없이 컴파일돼는 이유는 앞서 말한' 일련의 규칙 '에 따라 컴파일러가

알아서 캐스팅을 수행하기 때문이다.

 

그러니깐 Base( 기반 클래스 )타입 포인터로 Drived 객체( 파생 클래스 )를 가르키는 상황은

컴파일러 입장에선 자신이 갖고 있는 ' 일련의 규칙' 에 포함돼 있다는 소리다.

 

또 다른 예를 들어 봅시다.

어떤 상수 타입의 객체에 대한 참조( 또는 포인터 ) 는 초기화 될시

비 상수 객체를 가르킬수 있다.

 

class CObject{ ... };

 

CObject nonConstObject;                                // 비 상수 객체

 

const CObject* cpObject = &NonConstObject;    // 상수 객체를 가르키는 포인터로 비상수 객체를

                                                                    // 가르켰지만 이상 없이 통과!

 

요것 또한 컴파일러의 ' 일련의 규칙 '

- 즉, 상수 객체를 가르키는 포인터( 참조 )를 초기화 할시 비 상수 객체를 이용하는 것이 가능하다 

에 포함돼니 컴파일러가 발벗고 나서 자동적으로 캐스팅한 결과 이상없이 컴파일돼는 것이다.

 

 

암시적인 타입 변환에 사용자가 개입(?)

 

컴파일러에 내장된( 즉 하드 코딩된 ) 암시적 타입변환에 대한 규칙 자체는 바꿀수 없다.

그러나 프로그래머가 몇가지 방법을 이용해

컴파일러가 암시적인 타입변환을 할수 있게 끔 힌트를 줄수도 있는데

그 중 하나가 암시적 타입 변환 연산자라고 하는 것이다.

 

이 연산자의 형태는 operator 캐스팅할 타입() 인데

보다시피 반환 값 타입이 존재하지 않고 operator 키워드 옆에 달랑 타입( 캐스팅 ) 만

덩그러니 놓여져 있다.

 

바로 예제로 들어갑시다.

 

//---------------------------------------------

//   클래스 CTest 

//---------------------------------------------

class CTest

{

   public:
      operator float()
      {   return static_cast< float >( m_Var );   }
   private:
      static const int m_Var = 5;
};

 

 

/* Client Code */

 

float Result = 0.0f;
const float Radian = 3.14f;

 

Result = Radian * CTest();

 

실수형 타입인 Radian 을 CTest 객체( 임시 객체 ) 와 곱해

역시 실수형인 Result 변수에 그 결과를 대입하는 실질적인 의미가 전혀 없는 예제이다.

 

컴파일 에러가 발생하지 않는게 오히려 이상한 상황으로 보이겠지만

위 코드는 아무 문제 없이 컴파일이 성공한다.

그럼 과연 위의 코드가 컴파일돼면서 어떤 일들이 벌어졌는지 대충 살펴 봅시다.

 

실질적으론 연산이 이뤄지는 세번째 줄을 보면

첫번째로 컴파일러는 Radian 변수와 CTest 객체간의 곱셈을 시도하려 할것이다.

 

컴파일러는 Radian 의 타입이 float 실수형이 므로 당연히 두번째 피 연산자

즉, 곱셈 연산자( * ) 의 오른쪽 항은 당연히 float 가 될것이라고 예상하지만

안타 깝게도 오른쪽 항엔 왠 CTest 타입의 객체가 자리를 잡고 있다.

 

당황스런(?) 컴파일러는 이제부터 바쁘게 CTest 타입이 float ( Radian 의 타입 ) 타입으로

변환 될수 있는지 알아보러 다니기 시작한다.

 

맨 먼저 컴파일러에 내장된 암시적 타입 변환이 일어 날수 있는 규칙 을 살펴보지만

거기에 사용자 정의 타입인 CTest 에 관한 부분이 있을 턱이 없다.

 

그 다음으로 찾게 돼는 부분이 바로 ( 피연산자에 대한 )암시적 타입변환 연산자 이다.

다행히도 operator float() 란 놈이 있으니 컴파일러는 이제야 안도의 한숨을 쉬며

잽싸게 CTest 객체를 float 형으로 변환하고 무사히 위의 연산은 수행된다.

 

 

매개 변수가 하나인 생성자

 

컴파일러가 암시적인 타입변환을 할수 있게 끔 빌미( ? )를 제공하는 두번째 방법은

매개 변수 를 하나 받는 생성자를 정의 하는 것이다.

 

* 이제 부터 사용할 예는 EC++ 항목 24의 것을 거의 그대로 가져오는것이다.

 

//-------------------------------------

//   유리수를 추상화한 Rational 클래스

//-------------------------------------

class Rational

{

   ...

   //----------------------------------

   //   생성자

   //----------------------------------

   public:

      Rational( int numerator, int denominator = 1 );     // 분자, 분모( = 1 )를 취하는 생성자

   ...

   //----------------------------------

   //   연산자 재정의

   //----------------------------------

   public:

      void operator=( const Rational& rhs );                // 대입 연산자

      void operator*( const Rational& rhs );                // 곱셈 연산자

   ... 

};

 

유리수 체계를 추상화한 Rational 클래스는

생성자의 인자로 분자, 분모에 해당하는 정수를 받는다.

그런데 보다시피 분모인 denominator 는 디폴트 값 1을 취하므로

기본적으로 이 생성자는 하나의 인자를 받는것으로 볼수 있다.

Rational 클래스를 사용하는 예는 아마도 다음과 같을것이다.

 

Rational a( 3, 4 );    // 유리수 3 / 4

Rational b( 2 );        // 유리수 2 / 1, 즉 그냥 정수 2

 

일반적인 상식적으로 정수와 유리수와의 연산( 곱셈, 덧셈 등 )은 전혀 이상한 일이 아니다.

 

b * 3;

 

그러니깐 위와 같은 일,

유리수 b( 사용자 정의 )  와 정수 '3' ( 기본 형 )를 곱하는 것이 유효한 식이

아무래도 자연스럽다는 것이다.

 

상식적으로 보자면 둘의 타입은 전혀 호환 될것 같아 보이지 않아서

위의 식은 보기 좋기 실패할것 같지만 컴파일러에겐 유효한 식이다.

그럼 과연 뭐가 어떻게 돌아가길래 위 식이 성공하는지 살펴 봅시다.

 

일단 컴파일러 입장에서 위 식은 다음과 같이 해석된다.

 

b.operator*( 3 );    // b( Rational 클래스 )의 operator '*' () 이 호출

 

문제는 Rational 클래스의 operator '*' 는 정수를 취하지 않는다는것이다.

 

      void operator*( const Rational& rhs );                // 곱셈 연산자

 

보다시피 Rational 클래스의 정의된 operator '*' 연산자는 인자로 Rational 객체의 참조를 받고 있다.

유리수 객체( 참조 )가 들어갈 자리에 정수를 넣었는데도 컴파일 상 아무런 문제가 없다는것은

결국 컴파일러란 놈이 정수를 유리수 객체 타입으로 암시적 캐스팅을 수행했다는 것이다.

 

그럼 대체 어떻게 정수( int )가 유리수( Rational ) 객체로 탈바꿈 할수 있는것인가?

 

범인은 바로 요놈이다.

 

      Rational( int numerator, int denominator = 1 );     // 분자, 분모( = 1 )를 취하는 생성자

 

앞서 말했다시피 매개 변수가 하나인 생성자는 컴파일러가 암시적인 타입변환을 수행하는데

힌트를 제공한다고 했다.

 

이 생성자는 정수를 Rational 타입으로 바꾸는 역할도 수행한다.

 

즉, 매개변수가 하나인 생성자는 그 매개 변수 타입을 자신의( 생성자가 속한 ) 클래스 타입으로

변환 가능하다는 조건을 컴파일러에게 제공한다는 얘기다.

 

그러니까 결과적으로 위의 b * 3 이란 식은 컴파일러가 다음과 같이 해석한다는 거다.

 

       b.operator*( Rational( 3 ) );

 

그렇다고 ' 그렇다면 정수 타입은 무조건 Rational 객체로 바뀔수 있는 거군. 훗... 역시 난 천재'

라고 생각하면 좀 곤란한다.

 

위의 ' 매개 변수가 하나인 생성자를 통한 타입 변환 '이 성공하기 위해선 한 가지 조건이 필요하다.

- 뭐 앞에서 설명하는 과정에 이미 들어갔지만

 

그 조건은 바로 ' Rational 객체를 매개 변수로 갖고 있는 연산자가 호출될 경우' 이고

정수 타입의 인자가 Rational 타입인 매개 변수에 전달될때

비로서 정수 타입이 Rational 타입으로 캐스팅 된다는 것이다.

 

앞서 봤다시피 b * 3 은 우선 ' b.operator( 3 ) ' 으로 해석된다고 했는데

이때 operator '*' 는 const Rational& 타입을 인자로 취한다고 했다.

이상태에서 정수( 3 )은 const Rational& 타입의 임시 변수에 대입돼는데

- 인자 전달 매커니즘 상

이때야 비로소 ' Rational( int ) ' 생성자가 개입(?)해 정수 형을 Rational 타입으로

바꿀수 있다는 것이다.

 

가령, b * 3 을 뒤집어 3 * b 를 만들어 버리면 결과는 어떻게 될것인가?

 

어떻게든 될거라고 생각 했겠지만 얄짤없이 컴파일 실패가 발생한다.

 

위에서 정수(타입)은 Rational 타입으로 암시적 변환된다고 했지만( Rational( int ) 에 의해 )

전제 조건을 만족 시키지 못했다. 즉, 정수 3 이 Rational 객체로 변환 되기 위해선

그 자리에 Rational 객체 타입을 인자로 받는 operator '*' 가 호출되야 한다.

 

그러니까 이런 놈이 필요하다고 쩝...

 

const Rational operator*( const Rational& Arg1, const Rational& Arg2 );

 

그러면 컴파일러의 해석상 operator*( 3, b ) 이렇게 될것이고

 

3 이 전달될 자리의 매개 변수 타입이 const Rational& 이므로 적법한

( 암시적인 )타입 변환이 발생해 위식은 무사히 컴파일 된다.

- 여기서 b 야 원래 Rational 객체니 두말하면 잔소리고

 

정수 3이 들어갈 자리에 Rational 객체를 받는 '*' 연산자가 필요하다고 했는데

일단 이 '*' 연산자는 멤버 '*' 연산자던 , 전역 '*' 연산자던 상관 없지만 3 이 클래스가 아니므로

저 식이 유효하려면 이경우 무조건 전역 '*' 연산자 만 가능하다.

 

만일, 매개 변수가 하나인 생성자가 암시적 타입변환에 대한 빌미를 제공하는 것에 대해

불만이 있는 사람이라면 ' explicit ' 키워드를 생성자 앞에 붙이도록.

 

      explicit Rational( int numerator, int denominator = 1 );     // 분자, 분모( = 1 )를 취하는 생성자

 

이러면 앞서 가능했던 부분( int 를 Rational 로 )이 불가능해진다.

즉, b * 3 이란 식은 가차없이 컴파일 에러로 지목된다는 말이다.

 

물론 암시적 타입 변환이 좋을 마냥 좋을 경우만 있는 것은 아니므로 explicit 키워드로

막아야 될 때도 있을것이니 알아서 상황에 맞게 조율하도록 합시다.
 

반응형

+ Recent posts