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

C++/Effective C++

2020. 8. 27. 22:29

항목 20에서 '값에 의한 전달보다는 상수 객체 참조자에 의한 전달을 선호하자' 라는 주제를 다루었다.

간결하면서 확실한 효과를 누릴수 있기에 모든 값전달을 상수 객체 참조자로 변환하려는 시도들을 보인다.

하지만, 다음과 같은 경우 상수 객체 참조자에 의한 전달이 의미 없을 뿐만 아니라, 오히려 일을 키우게 된다.

 

const Rational& operator* (const Rational& rhs, const Rational& lhs){

	Rational result(lhs.n*rhs.n, rhs.d*rhs.d);
	return result;
}

 

이 연산은 기존의 생성자를 호출하지 않도록 값에 의한 전달을 사용하지 않는 목적을 잃었다고 할 수 있다.

함수 내부에서 Rational의 생성자가 호출되는 것을 볼 수 있다. 하지만, 더 중요한 점은 지역 객체를 반환한다는 것.

지역 객체는 return이 되는 순간 생명을 다 한다. 이게 값에 의한 전달이었다면 상관이 없지만 상수 객체를 반환 하고 있다는 것이 포인트다. 즉, 아무 쓸모없는 바이트 덩어리의 메모리에 대한 참조자가 반환되기에 해당 함수를 사용하는 쪽에서 미정의 동작이 발생하게 된다. 

 

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

 

위의 코드는 힙에 할당하여 생명 주기를 함수 밖에까지 연장시킨 코드이다. 메모리 관련 항목을 읽은 우리들은 위의 코드에 내재된 문제점을 바로 발견할 수 있다. 해당 객체의 메모리 해제를 사용자에게 떠넘긴 것. 메모리 누수의 가능성을 품고 있다. 그리고 만약 operator* 연산이 두 번 일어날 경우 delete도 두 번을 해주어야 한다는 것도 잊어서는 안된다!

 

Rational w,x,y,z;

w = x * y * z;

 

첫 번째 방법이든 두 번째 방법이든 생성자를 반드시 호출하게 된다. 결국 처음에 말했듯이 생성자 호출을 막기 위해 상수 객체를 넘기려 했던 일이 무로 돌아가는 상황인 셈이다. 여기서 포기하지 않고 정적 객체를 사용하는 용감한 코드를 한 번 보자.

 

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

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

	Rational a,b,c,d;
	...
	if ((a*b) == (c*d)){
	}else{}

 

먼저 정적 객체를 사용하는 경우 스레드 안정성 문제가 얽혀 있다. 이보다 더한 문제는 밑의 연산인데

밑의 조건문 if ( (a*b) == (c*d)) 는 무조건 참이다. opeartor ==의 계산 과정에서 operator*의 연산이 일어날테고

operator ==이 비교하는 피연산자는 operator* 안의 정적 Rational 객체의 값, 그리고 operator* 안의 정적 Rational 객체의 값이다. '정적' 객체임을 잊지 말자! 이 연산은 애초에 같은 값을 비교하는 것이나 마찬가지이다!

 

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

 

위의 예시들로부터 생성자는 반드시 한 번 생성이 된다. 이는 당연한 지불비용이며 다른 문제점을 갖는 불안정한 코드를 쓸 바에야 값에 의한 전달 그대로 놔두는 것이 맞는 표현이 되겠다.

 

그리고, 위의 코드는 RVO(return value optimization)가 적용되어 성능을 높이는 최적화가 이루어 진다. 위의 경우에는 무명 객체를 반환하면서 컴파일러가 opeartor*의 반환 값에 대한 생성과 소멸 동작을 안전하게 제거해 준다.

 

이것만은 잊지 말자!

 

  • 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지 마세요(항목 4를 보시면 지역 정적 객체에 대한 참조자를 반환하도록 설계된 올바른 코드 예제를 찾을 수 있습니다. 최소한 단일 스레드 환경에서는 통합니다.)