[Effective C++] 항목21 : 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자

Jangmanbo·2023년 4월 12일
0

Effective C++

목록 보기
21/33

항목 20을 본 후 pass by value를 척살하려는, 모든 코드를 오직 reference to const로만 전달하려는 사람들도 있을 것이다. 이러한 사고방식은 좋지 않다.

class Rational {

public:
	Rational(int numerator = 0, int denominator = 1);	// 생성자가 explicit이 아닌 이유는 항목 24
    ...
private:
	int n, d;	// 분자, 분모
   
	// 반환 타입이 const인 이유는 항목 3
	friend const Rational& operator*(const Rational& lhs, const Rational& rhs);
};

operator* 함수가 반환하는 참조자는 이미 존재하는 Rational 객체를 참조한 이름이다. 따라서 이 operator* 함수에서 직접 Rational 객체를 생성해서 반환해야 한다.



참조자를 반환하는 함수 구현하기

함수 수준의 객체 생성 방법에 따라 설명한다.

1. 스택에 생성

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

참조자를 반환하기로 구현한 의도는 생성자가 불리는 것을 막기 위해서였다. 그러나 결국 함수 내에서 Rationl객체를 생성하게 됐다. 여기서부터 잘못됐다.

문제는 이후에 발생한다. 함수 내에서 생성한 result는 지역 객체이므로 operator*가 끝나는 순간 소멸자가 호출되어 버린 바이트 덩어리일 뿐이란 것이다.


2. 힙에 생성

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
}

함수가 반환할 객체를 힙에 생성했다가 그 객체의 참조자를 반환하는 건 어떨까?

스택에 생성하는 방법과 마찬가지로 일단 생성자가 호출된다.
문제가 더 있다. new로 할당한 객체는 누가 delete한단 말인가.

예시

Rational w, x, y, z;
w = x * y * z;		// operator*(operator*(x, y), z)

w = x * y * z; 이 한 문장에서만 operator*가 2번 호출되기 때문에, new에 짝을 맞추어 delete를 호출도 2번 수행해야 한다. 그러나 본문 어디에서도 delete를 할만한 방법이 없다. operator*에서 반환하는 참조자 뒤에 숨겨진 포인터에 대해 사용자가 접근할 방법이 없기 때문이다.

스택 혹은 힙에 객체를 생성하는 방법 2가지 모두 문제는 동일하다. 기존의 의도는 생성자를 호출하지 않는 것이었지만 결국 생성자를 호출한다는 것이다. (+다른 문제도 발생)



3. 함수의 정적 객체 정의

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	static Rational result;	// 반환할 참조자가 기리킬 정적 객체
    
    result = ...;
    
    return result;
}

그렇다면 Rational 객체를 정적 객체로 정의하고 이 객체를 참조자로 반환하면 어떨까?
어떤 코드에서 operator*를 호출하더라도 반환값(result)은 항상 동일할 것이다.

예시

bool operator==(const Rational& lhs, const Rational& rhs);

Rational a, b, c, d;
...

if ((a*b)==(c*d))	// operator==(operator*(a, b), operator*(c, d))
{
	// 비교해서 같을 때 처리
}
{
	// 비교해서 다를 때 처리
}

바로 여기서 operator*(a, b)operator*(c, d)의 반환값이 항상 동일하다는 것이다.
둘 다 operator*의 정적 객체인 result를 반환하므로 당연한 결과다.



해결법: 새로운 객체 반환

inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);	// 새로운 객체를 생성하여 반환하기
}

물론 이 코드에는 반환값을 생성 및 소멸하는 비용이 든다. 그러나 이 비용은 올바른 동작을 위해 지불하는 작은 비용일 뿐이다.

또한 대부분의 컴파일러는 RVO(return value optimization)을 제공하여 반환값에 대한 생성과 소멸 동작을 안전하게, 빨리 수행한다.

참고: C++ RVO, NRVO에 대해 알아보자




결론: 참조자를 반환하든, 객체를 반환하든 올바른 동작이 이루어지도록 하자!

  • 절대 하지 말아야 할 것
    • 지역 객체에 대한 포인터/참조자로 반환
    • 힙에 할당된 객체에 대한 참조자 반환
    • 지역 정적 객체에 대한 포인터/참조자 반환

0개의 댓글