항목 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 ... )
문제는 어떤 식으로 선언해도 문제가 된다는 것이다.
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 에서 반환 되는 참조자는 이미 소멸된 객체를 가리키는 참조자가 된다.
즉, 프로그램은 미정의 동작을 하게 된다는 말이다.
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;
이렇게 활용한다면, 더욱 메모리 누수를 막을 방법이 없어진다.
참조자 뒤에 숨어있는 포인터에 대해서 사용자가 접근할 방법이 없기 때문이다.
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 해주는 방법도 있다. )
항목 23) 멤버 함수보다는 비멤버 비프렌드 함수를 사용하자. (0) | 2022.08.27 |
---|---|
항목 22) 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2022.08.27 |
항목 20) 값 전달 보다는 상수객체 참조자 전달 방식을 고려하자. (0) | 2022.08.27 |
항목 19) 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2021.12.05 |
항목 18) 좋은 인터페이스 설계: 제대로 쓰기에는 쉽게, 엉터리로 쓰기에는 어렵게 (0) | 2021.12.05 |
댓글 영역