앞 장에서 배운 내용으로 모든 코드를 '참조에 의한 전달'만으로 구성하려고 할 것이다.
그러나 함수 반환의 경우에는 참조자를 넘기는 것은 좋지 않다.

 

유리수를 나타내는 클래스가 하나 있고 두 유리수를 곱하는 멤버 함수가 있다고 가정하자.

 

1
2
3
4
5
6
7
8
9
10
11
class Rational
{
public:
    Rational(int numerator = 0int denominator = 1//분자, 분모
 
private:
    int n,d; 
 
friend
    const Rational& operator*(const Rational& lhs, const Rational& rhs); //두 곱셈 결과를 반환.
};
cs

 

배운대로 '참조에 의한 전달'을 했지만 생각해보면 위의 코드는 이상한 코드이다.
참조자는 반드시 이미 존재하는 Rational 객체의 참조자여야 한다, 그렇다면 대체 반환할 때 이미 존재하는 어떤 Rational 객체를

반환해야 된다는 것인가??

 

1
2
3
4
5
6
7
void Func()
{
    Rational a(1,2);   // a = 1/2
    Rational b(3,5);   // b = 3/5
 
    Rational c = a*b;  // c = 3/10이 될 것이다. 
}
cs

 

위의 코드를 보면 a*b위의 결과를 바로 c의 객체에다가 집어넣는다.
a*b의 결과 값을 가지는 Rational 객체가 존재해야지만 참조형으로 반환을 해줄 수 있을텐데 a*b의 결과인 임시 객체에 참조를

한다는 것은 아무런 의미가 없는 행동이다.

 

잘못된 시도1. 지역 객체를 만들어서 반환해보기.

 

아래와 같은 미친짓은 하지 말자.

 

1
2
3
4
5
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result(lhs.n*rhs.n. lhs.n*rhs.n);
    return result;    //미친짓
}
cs

 

위와 같은 경우는 지역 객체이고 operator* 함수가 끝나면 result 객체도 자연히 소멸되는데 저 소멸되는 객체의 참조자를

반환하는 것은 절대 하지 말아야 한다.

또한 애시당초 참조자를 전달하는 목적은 생성자를 호출하지 않기 위함인데 위와 같은 코드는 어차피 생성자도 호출되기 때문에

'참조에 의한 전달'이 의미가 없게 된다.

 

잘못된 시도2. new로 힙에 메모리를 생성해서 반환하기.

 

1
2
3
4
5
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational* result = new Rational(lhs.n*rhs.n. lhs.n*rhs.n);
    return result;    //이것도 미친짓
}
cs

 

다른 방법으로 힙에 메모리를 생성하여 소멸되지 않도록 해보려고도 할 것이다.

그러나 어차피 이 방법도 생성자가 호출되며 더 큰 문제는 해당 객체를 누가 delete를 해줄 것인지에 대한 문제이다.

만약 위와 같은 코드를 만든 상태에서 아래와 같이 함수가 호출되었다고 가정하자.

 

1
2
3
4
5
6
void Func2()
{
    Rational w. x. y. z;
 
    w = x * y * z; //operator*(operator*(x,y), z)와 같다.
}
cs

 

위의 x * y * z; 문장에서 operator*가 2번 호출되어 new가 2번 실행했으니 delete 또한 2번 해줘야 하는데....

프로그래머는 위의 객체에 접근할 방법이 없기 때문에 delete를 해줄수가 없다.

 

잘못된 시도2. 정적 객체를 만들어 정적 객체를 반환하기.

 

그래서 마지막으로 한 번 더 시도하게 되는 것이.... static일 것이다.

 

1
2
3
4
5
6
7
8
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    static Rational result;
 
    result = ...;
 
    return result;    //가장 미친짓
}
cs

 

위의 코드는 스레드 안정성 문제가 얽혀 있다.
또한 만약 아래와 같은 코드가 진행된다면 정상적으로 진행되지 않을 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
void Func2()
{
    Rational a. b. c. d;
 
    ...
 
    if((a*b) == (c*d))
    {
        //두 유리수 곱이 같으면 어떤 처리를 한다.
    }
}
cs

 

위의 operator== 부분 코드를 바꿔보면 아래와 같다.

 

1
if (operator==(operator*(a,b), operator*(c,d)));
cs

 

a,b,c,d에 적절한 값이 들어가고 2번의 operator* 연산이 진행되어 정적 객체가 각 각 반환되었을 것이다.

그리고 operator== 함수가 호출되어 두 개의 값이 같은 비교를 할 것이다.

그런데 a,b,c,d 값이 무엇이 되었든 간에 정적 Rational 객체의 값이 반환되므로 무조건 마지막에 계산된 operator*의

반환 값이 Rational 객체의 값으로 설정되어 있을 것이다.

 

즉, a,b,c,d 값에 상관없이 operator* 의 반환 값은 항상 일치하므로 상기의 조건문은 무조건 true가 된다.

 

해결 방법

 

위의 잘못된 시도들에서 봤듯이 무조건 '참조에 의한 전달'만이 옳은 것은 아니다.

그러므로 아래와 같이 새로운 객체를 반환하게 만들어 반환하도록 하자.

 

1
2
3
4
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);    //가장 미친짓
}
cs

 

이 방법 또한 새로운 객체가 생성되는 것이므로 생성자와 소멸자가 호출된다.

그러나 위와 같이 에러를 유발하게 되는 위험 부담은 일체 없다. 컴파일러 구현자들이 가시적인 동작 변경을 가하지 않고도

기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 해두었다. 그러므로 상기 동작은 생각보다 빠르다.

 

참조자를 반환할지 새로운 객체를 반환할 것인가를 결정할 때, 올바른 동작이 이루어지도록 만들어야 한다는 것은 반드시

명심해야 한다.

 

1줄 요약

 

 - 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 힙에 할당된 객체에 대한 참조자를 반환하는 일 또는

   지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면

   절대로 하지 마라.

+ Recent posts