- 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 객체가 두 개 이상 필요해질 가능성이 있다면 절대 하지 마라.
지역 정적 객체에 대한 참조자를 반환하는 것은 괜찮을 수 있다(단일 스레드 한정)
함수에서 객체에 대한 참조자 반환 금지
값에 의한 전달이 효율적으로 문제가 된다는 사실 때문에 모든 코드를 참조에 의한 전달을 시도하려 할 수 있다.
하지만 문제가 생길 수 있기 때문에 조심해야 한다.
유리수를 나타내는 클래스가 있다고 가정하자. 유리수를 곱하는 opeartor*가 있다.
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
...
private:
int n, d;
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};
이 클래스의 operator*는 곱셈 결과를 반환할 것이다.
이 과정에서 Rational 객체의 생성과 소멸에 비용이 들어간다. 이 비용을 줄이기 위해 참조자로 반환한다고 하면 문제가 발생할 수 있다. 참조자는 실제로 존재를 가리키는 포인터 같은 것이다. 그래서 실제 객체를 만들어 참조자를 반환하려는 시도를 한다면 문제가 발생할 수 있다. 함수 수준에서 새로운 객체를 만드는 방법은 스택에 만드는 방법과 힙에 만드는 방법이 있다.
스택에 만들었을 때(지역 객체)
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
이 경우는 스택에 객체를 만든 지역 객체인데 이 객체는 함수가 끝날 때 소멸된다. 즉, opearator*는 없어지는 객체의 참조자를 넘겨주게 된다.
힙에 만들었을 때
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
이 경우는 힙에 객체를 생성한 경우인데 이를 반환하면 delete가 불러지는 것을 장담하지 못해 메모리 누출이 생길 수 있다.
Rational w, x, y, z;
w = x * y * z; // operator*(operator*(x, y), z)
operator*가 두 번 호출되고 있기 때문에 new/delete도 두 번 필요하다. 그런데 사용자 측에서는 불가능하다.
operator*로부터 반환되는 참조자는 단 하나이다. 사라진 하나의 참조자는 그대로 누출된다.
또한, 위의 방법들은 초기 목적이었던 객체의 생성, 소멸 비용을 줄이는 것도 할 수 없다. 모두 객체를 생성하고 있기 때문이다.
정적 객체로 만들었을 때
Rational 객체를 정적 객체로 함수 안에 정의해 놓고 참조자를 반환하는 식으로 구현하면 안 된다.
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
{
//다른 경우
}
(a * b)의 결과로 반환된 정적 객체와 (c * d)의 결과로 반환된 정적 객체 모두 operator*안에 있는 정적 객체로 같은 객체이다. 같은 객체를 가리키는 문제를 피하기 위해 배열로 처리하려 할 수 있지만 이 배열의 크기를 정하는 것도 문제이며 생성/소멸 비용을 아끼려 했던 목적에서 크게 벗어난다.
올바른 사용
정리하자면, 새로운 객체를 반환해야 하는 함수에서는 새로운 객체를 생성해야 한다.
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, rhs.d * rhs.d);
}
참조자를 반환할 것인가 아니면 객체를 반환할 것인가를 결정할 때, 올바른 동작이 이루어지도록 만드는 것이 제일 중요하다. 비용은 그다음에 고려해야 할 사항이다.