항목 21 함수에서 객체를 반환할 경우에는 참조자를 반환하려고 하지 말자
값에 의한 전달에 숨겨진 효율 문제를 알아챈 프로그래머 중에는 코드에 멀쩡하게 들어있는 '값에 의한 전달'을 '참조에 의한 전달'로 모조리 바꾸어 실제로 있지도 않은 객체의 참조자를 넘기는 경우가 있다. 예를 들어, 어떤 유리수를 나타내는 클래스가 있다고 가정해보자. 이 클래스에는 두 유리수를 곱하는 멤버 함수가 선언되어 있다.
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*는 곱셈 결과를 값으로 반환하도록 선언되어 있다. 여기서 값이 아닌 참조자를 반환하도록 할 수 있을까? 참조자에 대해 한 번만 더 생각해보자. 참조자는 이미 존재하는 객체에 붙는 또 다른 이름이다. operator*가 참조자를 반환하도록 만들어졌다면 이 함수가 반환하는 참조자는 반드시 이미 존재하는 Rational 객체의 참조자여야 한다. 그러면 한번 유리수 객체에 대한 참조자를 operator*에서 반환할 수 있도록 유리수 객체를 직접 생성해보자.
21.1 객체를 참조자로 잘못 반환하는 경우
■ 21.1.1 지역 스택 객체
함수 수준에서 새로운 객체를 만드는 방법 중 하나는 스택에 만드는 것이다. 스택에 객체를 만들려면 지역 변수를 정의해야 한다.
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); /* 유효 범위를 벗어나면 메모리에서 사라집니다. */
return result;
}
일단 생성자를 호출하기 싫어서 시작한 일인데, 결국 result가 다른 객체처럼 생성되어야 한다. 그리고 심각한 문제가 또 있다. 이 연산자 함수는 result에 대한 참조자를 반환하는데, result는 지역 객체이므로 함수가 끝날 때 소멸되어 버린다. 다시 말해, 이 operator*는 현재 온전한 Rational 객체에 대한 참조자를 반환하지 못한다. 이 Rational 객체는 곧 소멸자가 호출되어 흔적도 없이 사라질 메모리기 때문이다.
■ 21.1.2 힙 객체
함수에서 반환할 객체를 힙에 생성해 두었다가 그 참조자를 반환하는 것을 어떨까? operator* 함수를 힙 기반으로 만들어보자.
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); /* 객체가 소멸되지 않아 메모리 누출이 일어납니다. */
return *result;
}
여전히 생성자가 한 번 호출되기는 마찬가지다. 또한, new로 생성한 result 객체를 어디에서도 delete로 뒤처리하지 않아 메모리 누출이라는 문제가 발생한다. 사용자는 operator*로부터 반환되는 참조자 뒤에 숨겨진 포인터에 대해 어떻게 접근할 방법이 없기 때문에 자원 누출에 속수무책이 된다.
■ 21.1.3 지역 정적 객체
Rational 객체를 정적 객체로 함수 안에 정의해 놓고 이것의 참조자를 반환하도록 operator*를 작성해보자.
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
static Rational result; /* 반환할 참조자가 가리킬 정적 객체입니다. */
result = ...; /* lhs와 rhs를 곱한 결과를 저장합니다. */
return result;
}
이제 아래 준비한 코드를 보면 a, b, c, d에 어떤 값이 들어가더라도 ((a * b) == (c * d)) 표현식은 항상 참(true)이 되어 버리는 문제가 발생한다. operator==이 비교하는 피연산자는 operator* 안의 정적 Rational 객체의 값과 operator* 안의 정적 Rational 객체의 값이므로 결과적으로 두 객체는 항상 같기 때문이다.
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
if ((a * b) == (c * d)) { ... } /* 항상 참인 조건문이 되어 버립니다. */
// if (operator==(operator*(a, b), operator*(c, d)))입니다.
else { ... }
21.2 새로운 객체 반환
객체를 반환해야 하는 함수를 작성하는 방법은 바로 새로운 객체를 반환하는 것이다. Rational의 operator*는 아래처럼 작성해야 한다.
inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
참조자를 반환할 것인지 객체를 반환할 것인지 결정할 때, 어떤 선택을 하든 올바르게 동작하도록 만드는 것이 여러분이 할 일이다. 선택한 결과를 저비용으로 만들 방법은 신경 쓰지 않아도 된다.
NOTE
① 지역 스택 객체, 힙에 할당된 객체, 그리고 지역 정적 객체의 포인터나 참조자를 반환하는 일은 절대로 하지 말자.
'Object Oriented Programming(C++) > Effective C++' 카테고리의 다른 글
Effective C++ | 항목 23 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2022.02.15 |
---|---|
Effective C++ | 항목 22 데이터 멤버가 선언될 곳을 private 영역임을 명심하자 (0) | 2022.02.11 |
Effective C++ | 항목 20 값에 의한 전달보다는 상수 객체 참조자에 의한 전달이 대개 낫다 (0) | 2022.02.03 |
Effective C++ | 항목 19 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2022.02.03 |
Effective C++ | 항목 18 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2022.02.03 |
댓글