본문 바로가기

STUDY/C++

사용자 정의 타입변환과 explicit

 

C++ 은 컴파일러가 암시적인 타입변환을 할 수 있도록 합니다. 

char를 int로 short를 double로 double을 short로 변환이 가능하며 별 문제가 생기는 경우도 없습니다 

standard한 타입에 대해서는 자유로운 형변환을 막을 방법이 없지만, 우리가 직접 만드는 타입에 대해서는 형변환 결정권은 우리가 가질 수 있습니다.

컴파일러가 사용하는 타입변환 함수는 두가지입니다.

 

1. 단일인자 생성자

2. 암시적 타입변환 연산자


1. 단일인자 생성자

 

 

암시적 타입변환 연산자는 operator만 붙어있는 꼴입니다. 이 함수는 반환값 타입이 없습니다. 

아래 Rational class에 있는 operator double이 암시적 타입변환 연산자입니다. 

 

class Rational
{
public:
	Rational(int numerator = 0, int denominator = 1)
	{
		_rational =  (double)numerator / (double)denominator;
	}

	operator double() const
	{
		return _rational;
	}
private:
	double _rational;
};

 

이런 변환은 Rational 객체와 다른 타입의 연산을 수행하는 부분에서 유용하게 사용할 만 합니다. 

암시적 타입변환 연산자는 아래의 코드에서 r과 0.5를 곱할 때 호출이 됩니다. 

 

operator double()이 없다면 아래의 코드는 빌드에러가 나겠죠.

 

int main(void)
{
	Rational r(1, 2);
	double d = 0.5 * r;

	cout << d << endl;
}

 

타입변환 함수는 제공할 때 많은 주의를 하고 제공을 해야 합니다. 

위의 타입변환 함수가 호출되는 경우는 아마 프로그래머의 의도대로 동작을 한 경우일 것입니다.

하지만 아래의 경우를 볼게요. 

 

int main(void)
{
	Rational r(1, 2);

	cout << r; 

	return 0;
}

 

우리는 operator<<함수를 정의하지 않았지만, 빌드에러도 나지않고 실행도 됩니다. 

이 경우에도 역시 우리가 의도하지 않았지만 operator double() 이 호출됩니다. 

 

컴파일러는 호출을 operator<<가 없지만 어떻게든 함수 호출을 성공시키기 위해 암시적 타입변환 함수들을 찾아 맞춰보다가 operator double() 을 찾게 되고, double타입이 출력되는 것으로 성공합니다.

우리의 의도되로 호출된것이 아니라는 점에 초점을 맞추어야 합니다.

 

해결방법은 다음과 같습니다. 

class Rational
{
public:
	Rational(int numerator = 0, int denominator = 1)
	{
		_rational =  (double)numerator / (double)denominator;
	}

	/*operator double() const
	{
		return _rational;
	}*/

	double asDouble() const
	{
		return _rational;
	}

private:
	double _rational;
};

int main(void)
{
	Rational r(1, 2);

	cout << r;					// 오류발생 
	cout << r.asDouble();		// 성공

	return 0;
}

 

operator double() 이 사라지면 기존의 cout<<r은 빌드에러가 납니다.

하지만 asDouble이라는 함수로 바꾸어, 불편하긴 하지만 의도치 않은 타입변환 함수가 호출되지 않도록 하는 것이죠.

 

예를 들면 string class에도 char*로 암시적 형변환이 불가능하게 하고, c_str()함수를 호출하게 하도록 되어 있습니다. 

 

 


2. 암시적 타입변환 연산자

 

단일인자 생성자를 통한 암시적 타입변환도 문제가 발생하기 쉽습니다.

예제를 살펴보겠습니다.

 

template를 사용한 Array 클래스를 하나 만들어보겠습니다.

생성자는 lowBound와 highBound를 가지는 범위지정 생성자와, size를 지정할 수 있는 생성자 두 가지가 있다고 가정합니다.

 

template<class T>
class Array
{
public:
	Array(int lowBound, int highBound) 
	{
		_minSize = lowBound;
		_maxSize = highBound;
		_size = _minSize;

		_array = new T[_minSize];
	};
	Array(int size) 
	{
		_minSize = size;
		_maxSize = size;
		_size = _minSize;

		_array = new T[_minSize];
	};

	T& operator[] (int index) const
	{
		return _array[index];
	}

	int size() const { return _size; };


private:
	T* _array;
	int _size;
	int _maxSize;
	int _minSize;
};

 

아래는 main문입니다. 

operator == 가 있고, 아래에 main문에 a==b[i]와 같이 말도안되는 코드가 있다고 생각해 볼게요. 

 

bool operator==( const Array<int>& rhs,  const Array<int>& lhs)
{
	//...
	if (rhs.size() != lhs.size())
	{
		return false;
	}
	for (int i = 0; i < rhs.size(); ++i)
	{
		if (rhs[i] != lhs[i])
		{
			return false;
		}
	}

	return true;
}

int main(void)
{

	Array<int> a(10);
	Array<int> b(10);

	for (int i = 0; i < 10; ++i)
	{
		if (a == b[i])
		{
			cout << "same" << endl;
		}
	}

	return 0;
}

 

의도는 main문에 있는 if(a[i] == b[i]) 를 1:1로 비교하는 것인데, 잘못 짰다고 가정해 볼게요. 

우리가 보기에는 말도안되는 코드이지만 컴파일러는 이 구문을 문제없이 실행합니다.

실제론 operator==(int rhs, Array<int>& lhs)라는 함수가 없지만 실행되는 이유는 뭘까요?

 

사실 if(a == b[i]) 구문은 아래와 같이 동작합니다.

if(a == static_cast<Array<int>>(b[i]))

 

그렇기 때문에 operator == (bool operator==( const Array<int>& rhs,  const Array<int>& lhs) 가 컴파일에러 없이 실행이 되는 것입니다.

우리가 의도하지 않아도, 컴파일러는 어떻게던 맞는 타입으로 호출하려고 합니다.

 

이 코드가 실제로 동작하면  if(a == b[i]) 구문이 불릴 때 마다 임시객체가 생성되어 10번만큼 생성과 소멸을 반복하겠죠. 정말 비효율 적입니다. 

 

암시적 타입변환 연산자는 다음과 같이 피할수 있습니다. 

explicit 키워드를 사용하면 위와같은 암시적 타입변환에 사용되지 않습니다. 

	explicit Array(int size) // 이 생성자는 암시적 타입변환 함수로 쓰일수 없다. 
	{
		_minSize = size;
		_maxSize = size;

		_array = new T[_minSize];
	};

 

 

Array<int> a(10); 과 같이는 사용할 수 있지만, 

 if(a == b[i]) 구문에서는 에러가 발생하게 되고 친절하게 빨간줄도 그어줍니다.

 


사용자 정의 타입변환 함수는 두개 이상 쓰이지 않습니다.

즉, 단일인자 생성자와 암시적 타입변환 연산자가 동시에 쓰이지 않는다는 것입니다.

클래스를 제대로 만들어 놓으면 암시적 타입변환을 허용하지 않도록 만들 수 있습니다.

 

template<class T>
class Array
{
public:
	class ArraySize
	{
	public:
		ArraySize(int numElements) :theSize(numElements) {};

		int size() const { return _size; };


	private:
		int _size;
	};

	Array(ArraySize size) // 이 생성자는 타입변환 함수로 쓰일수 없다.. 
		:_size(size)
	{
		_array = new T[_size.size()];
	};
...
}

 

 

컴파일러는 int를 ArraySize객체로 바꾼 다음에  그 객체로부터 Array<int>의 객체를 생성하는 처리는 할 수 없습니다.

이렇게 하기 위해서는 두번의 사용자 정의 변환을 거쳐야 하기 때문입니다.

규칙에 의해 이런일이 금지되어 있으므로 explicit를 사용하지 않아도 if(a == b[i]) 구문은 동작하지 않습니다.

 

ArraySize같은 클래스를 프록시 클래스 라고 하는데, 다른 객체를 대신한다고 해서 지어진 이름입니다.

 

 

 

결론: 암시적 타입변환을 허용하지 못하도록 타입변환 연산자를 왠만하면 제공하지 말자.