관리 메뉴

기억을 위한 기록들

[CPP-effective] 4-7 타입 변환이 모든 매개변수에 대해... 본문

C & CPP/Effective C++

[CPP-effective] 4-7 타입 변환이 모든 매개변수에 대해...

에드윈H 2021. 4. 19. 10:40

타입 변환이 모든 매개변수에 대해 적용되어야 한다면, 비멤버 함수를 선언하자.

 

예를 들어 유리수를 나타내는 클래스를 만들고 있다면, 정수에서 유리수로의 암시적 변환을 허용을 판단해도 크게 어이없거나 하진 않을 것이다. C++에서 제공하는 int -> double 변환과 별반 다르지 않다.

 

유리수를 나타내는 클래스를 하나 보자.

 

 

class Rational {
public:
	Rational(int num = 0, int denom = 1); //num=분자 / denom=분모


	const Rational operator*(const Rational& rhs) const;

	int GetNum() const;
	int GetDenom() const;
private:
	//...
};

 

유리수를 나타내는 클래스이니, operator* 연산자도 포함시켜주었다. 이렇게 설계해두면 유리수 곱셉을 할 수 있게 된다.

 

int main()
{
	Rational oneEight(1, 8);
	Rational oneHalf(1, 2);

	Rational result = oneHalf * oneEight; 
	result = result * oneEight;

	return 0;
}

 

그러나 객체끼리의 계산만 하기엔 부족한 부분이 있다. 바로 혼합형(mixed-mode) 수치 연산이 불가능하다는 것이다.

Rational 클래스와 일반 int 정수와 같은 것을 곱하고 싶기 때문이다.

	result = oneHalf * 2; //성공!
    
	result = 2 * oneHalf; //에러!

그러나, 곱셈은 교환 법칙이 성립해야 하는데 위의 코드는 교환 법칙이 되지 않는다.

위 문제의 원인은 함수 형태로 바꾸어 써보면 드러나게 된다.

	result = oneHalf.operator*(2); //성공!
    
	result = 2.operator*(oneHalf); //에러!

 

첫 번째는 oneHalf 객체의 operator* 함수를 멤버로 갖고 있는 클래스의 인스턴스이므로 컴파일러는 이 함수를 호출한다.

하지만, 두 번째 줄에서 정수 2 는 클래스의 객체도 아니고 operator* 함수도 있을 리가 없기 때문이다.

 

그런데, Rational::operator*의 선언문을 보면 인자로 Rational 객체를 받도록 되어 있다. 그런데 2가 어디에선 먹히고 어디에선 안 먹힌다. 이유가 뭘까? 바로 암시적 타입 변환(implicit type conversion)때문이다.

 

컴파일러는 함수에 int를 넘겼고, 함수에선 Rational를 요구하는 건 알고 있으나, int를 Rational 클래스의 생성자에 주어 호출하면 Rational로 둔갑시킬 수 있다는 것이다. 그래서 컴파일러는 자기가 알고 있는 대로 한 것이다.

 

다시 말해 마치,

	const Rational temp(2);

	Rational result1 = oneHalf * temp;
        Rational result2 = oneHalf * 2;

위의 두 개의 result1와 result2를 똑같이 처리한 것이다.

물론 컴파일러가 이렇게 동작한 것은 명시 호출(explicit)로 선언되지 않은 생성자가 있기 때문이다.

Rational 생성자가 만약 명시 호출 생성자였으면 위의 코드는 컴파일되지 않았을 것이다.

class Rational {
public:
	explicit Rational(int num = 0, int denom = 1); //explicit 추가

//..
};

추가 해준 뒤 다시 컴파일해보면 이전과 달리

	const Rational temp(2);

	Rational result1 = oneHalf * temp; //에러!
        Rational result2 = oneHalf * 2; //에러!

 

컴파일 에러 발생

컴파일 에러가 발생하게 된다. 

 

이렇게 되면 혼합형 수치 연산에 대한 지원은 수포로 돌아가게 된다.....

우리는 동작도 일관되게 유지하고 혼합형 수치 연산도 제대로 지원하는 것이 목적이다. 컴파일되게 해야 한다.

다시 이전으로 돌아가서

	result = oneHalf * 2; //비명시호출 생성자와 함께 성공! 
    
	result = 2 * oneHalf; //비명시호출 생성자와 함께인데도 컴파일에러! 

 

알 수 있는 점은 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야만 한다는 것이다.

그러니까 호출되는 멤버 함수를 갖고 있는 객체에 해당하는 암시적 매개변수에는 암시적 변환이 먹히지 않는다.

첫 번째 문장이 컴파일되고 두 번째 문장이 되지 않는 이유도 바로 이것이다. 전자의 경우 매개변수 리스트에 있는 매개변수가 쓰이지만, 후자의 경우는 그렇지 않다.

Rational result1 = oneHalf * 2; 

해당 코드의 실행 과정을 보면,   oneHalf * 2;  에 있는 2가 

Rational 생성자 첫 번째 매개변수 num으로 2가 들어가게 된다.

 

혼합형 수치 연산을 지원해야 만한다!! 그러면 그 방법으로 operator* 연산자를 비멤버 함수로 만들어서, 컴파일러 쪽에서 모든 인자에 대해 암시적 타입 변환을 수행하도록 내버려 두는 것이다.

 

class Rational {
public:
	Rational(int num = 0, int denom = 1);
//...
}

const Rational operator*(const Rational& rhs, const Rational& lhs) //비멤버 함수!
{
	return Rational(rhs.GetNum() * lhs.GetNum(), rhs.GetDenom()* lhs.GetDenom());
}

 

	Rational oneHalf(1, 2);

	Rational result1 = oneHalf * 2; //원래 성공!
	Rational result2 = 4 * oneHalf; //이제는 성공!

 

 

다 된 것 같지만 걱정이 하나 있다. 비멤버로 선언된 operator* 함수를 Rational 클래스의 프렌드 함수로 두어도 될까?

해당 예제에서는 아니오 라고 해야 옳다. operator*는 완전히 Rational 클래스의 public 인터페이스만을 써서 구현할 수 있기 때문이다.

 

중요한 결론은 "멤버 함수의 반대는 프렌드 함수가 아니라 비멤버 함수이다"라는 점이다.

 

어떤 클래스와 연관 관계를 맺어 놓고는 싶은데 멤버 함수이면 안 되는(위와 같은 예제) 함수에 대해, 이런 것들을 무조건 프렌드로 만들어 버리는 게 해결책이 아니다. 프렌드 함수는 피할 수 있으면 피하자. 상황에 따라 무조건 해야 할 때도 있지만, 반대로 무조건 해야 한다는 것은 아니다. 

 

정리

* 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함)에 대해 타입 변환을 해 줄 필요가 있다면, 그 함수는 비멤버이어야 한다.