상세 컨텐츠

본문 제목

항목 21) 함수에서 객체를 반환해야 할 경우에 참조자를 반환하지 말자

C++/Effective C++

by DeaKyungLee 2022. 8. 27. 17:08

본문

참조자 반환 함수

 

항목 3) 에서 상수성에 대한 설명하는 부분에서도 언급했지만,
참조자를 반환하는 함수는 거의 대부분의 경우에 Trouble maker 가 될 확률이 높다.

class Rational {
public :
  Rational(int numerator = 0 , int denominator = 1);
  …
private :
  int n, d;
 
friend const Rational& operator* (const Rational& lhs, const Rational& rhs);
//반환값이 const, 항목 3) 참조 (ex. i+j = 5)
};

 

유리수를 표현하는 Rational  class 에서 두 유리수를 쉽게 곱하기 위해서 * 연산자를 오버로딩 했다.
문제는 operator* 함수에서 Rational 참조자를 반환한다는 점이다.
참조자는 단순히 어떤 객체의 또 다른 이름이다. 즉, 원본값이 존재하지 않으면 참조자는 의미가 없다.

Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5
Rational c = a * b; // c should be 3/10

 

실제 사용 코드가 위와 같다고 할 때, ( 아주 일반적인 사용 방식 )
operator* 함수는 어떤 식으로 구현을 해야 할까?
생각해보면 operator* 함수 안에서는 어떤 식이든 반드시 객체 선언이 이루어져야 한다. ( Stack, Heap, Data ... )
문제는 어떤 식으로 선언해도 문제가 된다는 것이다.

 

1. Stack 영역 선언 ( 지역 변수 선언 )

const Rational& operator*(const Rational& lhs, const Rational& rhs) // warning! bad code!
{
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}

문제되는 문법이 아니므로 컴파일러는 아무런 문제도 제기하지 않는다.
하지만 result 객체가 사용하는 시점에도 살아있을까?

const Rational &w = a * b; // c should be 3/10
이 시점에 a * b 에서 반환 되는 참조자는 이미 소멸된 객체를 가리키는 참조자가 된다.
즉, 프로그램은 미정의 동작을 하게 된다는 말이다.

 

2. Heap 영역 선언 ( new 객체 )

const Rational& operator*(const Rational& lhs, // warning! more bad
const Rational& rhs) // code!
{
    Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
}

 

힙에 선언된 객체를 활용하기 때문에 값을 제대로 출력하지만, 메모리 누수가 발생한다.
result 객체를 delete 할 방법이 없기 때문이다.
특히 사용하는 측에서
const Rational &w = a * b * c;
이렇게 활용한다면, 더욱 메모리 누수를 막을 방법이 없어진다.
참조자 뒤에 숨어있는 포인터에 대해서 사용자가 접근할 방법이 없기 때문이다.

 

3. Data 영역 선언 ( Static ) 

const Rational& operator*(const Rational& lhs, const Rational& rhs) // bad code!
{
    static Rational result; // 반환할 참조자가 가르킬 정적 객체
 
    result = ... ; // 값 저장
    return result;
}
 
...
 
 
Rational a, b, c, d;
if ((a * b) == (c * d)) {
    // ... some process
}
else{
    // ... some process
}

 

위의 예시에서 (a*b) == (c*d) 항상 true 이다.
static 변수에 대한 리턴값이기 때문에 어떤 값이 어떻게 들어가든 true일 수 밖에 없다.

 

새로운 객체를 반환하는 함수는 새로운 객체를 반환하는 작동 방식이 옳다.

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

 

위와 같은 형식으로 작성하는 것이 낫다는 말이다.
대부분의 컴파일러는 해당 상황에 대해서 최적화를 진행하기 때문에 ( 반환값 최적화 - RVO )
생각보다도 빠르게 수행이 가능하다.

( operator* 가 아닌 일반 함수라면 destination 변수를 참조 형식으로 받아서 해당 함수 내부에서 값을 setting 해주는 방법도 있다. )

 

Things to Must Remember

  • 지역 스택 객체나 힙, static 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대하지 말아야 한다.
  • 참조자 반환 함수에 대해서는 신중하게 생각하자.

관련글 더보기

댓글 영역