본문 바로가기
서적 정리/Effective C++

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

by 민돌이2 2021. 12. 15.

사실 나는 게임 프로그래밍 위주였기 때문에 이번 챕터에서 말하는 상황을 마주한적은 없었다. 지금까지 그저 DX라이브러리 사용자였기 때문이다. 하지만 최근 STL를 구현하면서 vector나 list를 구현하면서 operator를 구현할 일이 생겼는데 이런 생각을 해본적이 없었기에 이런일이 발생할 수 있구나 하면서 코드를 다시 보게 됐다.

 

어느 한 클래스에 대해서 operator를 구현할 때 암시적 변환이 일어나는 경우가 있다.

class Rational //Rational : 유리수
{
public:
	Rational(int num = 0, int den = 1)
		: n(num), d(den)
	{ }

	~Rational() {}

	int numerator() const { return n; }
	int denominator() const { return d; }

	const Rational operator*(const Rational& rhs) const 
	{
		return Rational(n * rhs.numerator(), d * rhs.denominator());
	}

private:
	int n, d;
};

opeator*같은 경우에 Rational 객체에 대한 곱셈에 사용될 것이다.

Rational a(1, 8);
Rational b(1, 2);

Rational result = a * b;
result = result * a;

아무런 문제 없이 컴파일되고 작동한다. 하지만 혼합형(mixed-mode) 수치 연산도 할 일이 있을 것이다.

Rational a(1, 8);

a = a * 2; //문제 x
a = 2 * a; //에러

a = a * 2;는 문제가 없는데 a = 2 * a;는 왜 에러가 발생할까?

 

이는 operator을 함수 형태로 바꾸면 알 수 있다.

a = a.operator*(2); //문제 x
a = 2.operator*(a); //에러

a.operator*(2);은 a 객체에 있는 Rational의 operator* 함수의 멤버 함수를 호출하고 있고, 2.operator*(a); 은 직접 int자료형을 뜯어보지 않았지만 당연하게도 사용자가 만든 객체에 대한 구현이 안되어 있을것이다.

 

컴파일러는 비멤버 버전의 operator*이나 네임스페이스 혹은 전역 유효범위에 있는 operator*까지 찾아본다고 한다.

a = operator*(2, a); //에러

사용자가 만든 객체에 대한 구현은 되어있을리가 없으니 위 코드도 에러가 발생한다.

 

근데 다시 생각해보자. 처음에 구현한 코드도 Rational 객체를 매개변수로 하고 있지 int형을 매개변수로 사용하고 있지 않는다. 근데 왜 컴파일이 될까? 이는 암시적 타입 변환(implicit type convertsion)때문이다. 컴파일러가 알게 모르게 애쓰고 있었단 소리다. 즉, int형인 2를 가지고 생성자를 호출하여 돌아가게 하고 있었다.

const Rational temp(2);

a = a * temp;

만약 Rational 클래스의 생성자를 명시호출(explicit)(*주 1)로 선언되어 있었다면, 불가능 했다.

실제로 테스트해보면 생성자에 explicit선언 하면 a = a * 2;마저 에러가 발생한다.

 

또한 위 코드 생성자를 보면 매개변수가 2개가 있고, 2개 모두 기본값을 주었는데, 두 매개변수의 기본값을 지워도 에러가 발생한다.

const Rational temp(2); //에러

매개변수 2개가 필요한데 1개밖에 없어서 이다.

 

지금까지를 정리하자면 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야만 한다. 즉, 호출되는 멤버 함수를 갖고 있는 객체에 해당하는 암시적 매개변수에는 암시적 변환이 먹히지 않는다.

 

혼합형 수치 연산을 지원하게 하려면, 즉 모든 매개변수에 대해 암시적 타입 변환을 수행하게 하려면 어떻게 해야할까?

답은 비멤버 함수로 만드는 것이다.

class Rational //Rational : 유리수
{
public:
	Rational(int num = 0, int den = 1)
		: n(num), d(den)
	{ }

	~Rational() {}

	int numerator() const { return n; }
	int denominator() const { return d; }

private:
	int n, d;
};

const Rational operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

int main()
{
	Rational a(1, 8);

	a = a * 2; //문제 x
	a = 2 * a; //문제 x

	system("Pause");
	return 0;
}

 

operator* 함수를 friend 함수로 두어도 될까?

friend 함수의 의미를 생각해보면 

operator*은 numerator 함수나 denominator 함수를 사용하고 있다. 즉 완전히 public 멤버 함수로 구현하고 있다.

멤버함수의 반댓말은 비멤버 함수라는 것을 기억하자.

 

이것만은 잊지 말자

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

 

(*주 1) 명시호출(explicit)

형 변환을 막는다. 예를 들어 생성자에 explicit 키워드의 유무로 비교하자면

class TEST
{
public:
	TEST(int a)
		: n(a)
	{}

private:
	int n;
};

이 클래스의 경우

TEST te = 100; //문제 x 암시적 형 변환이 이뤄짐

 

왜 이게 되는걸까? 

const TEST temp(100);
TEST t = temp;

이런식으로 복사 대입 연산를 하는것도 아니고 그냥 생성자에서 형 변환을 해준다.

TEST te(100);

하지만 명시호출 키워드를 사용한다면 불가능해진다.

class TEST
{
public:
	explicit TEST(int a)
		: n(a)
	{}

private:
	int n;
};

TEST te = 100; //에러

명시호출 에러 결과

728x90

댓글