본문 바로가기
C++/Effective C++

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

by 멍청이 개발자 2024. 1. 21.
반응형

자, 제목그대로 이번 항목은 함수에서 객체를 반환해야 할경우 참조자를 반환하려 하지 말라는 것이다. 아래 코드를 보도록 하자.

class MyNumber
{
public:
    explicit MyNumber(int value=0);
    ...
private:
    int d;
    
friend const MyNumber operator* (const MyNumber& lhs, const MyNumber& rhs);
};

자 이런식으로 사용자 정의 숫자 클래스를 만들고 operator*를 오버로딩하여 서로 곱할 수 있도록 만들었다. 자 여기서 우리는 operator*의 반환 값으로 참조자를 반환하면 복사비용을 줄일 수 있지 않나? 라고 생각할 수 있다. 그렇다면 참조자라는 것은 원본 객체가 있어야 하는데 도대체 어떤 객체에 대한 참조자를 반환하느냐는 것이다. 그래서 우리는 간단히 이런식으로 코드를 짤 수도 있다.

const MyNumber& operator* (const MyNumber& lhs, const MyNumber& rhs)
{
    MyNumber result(lhs.d * rhs.d);
    return result;
}

자, 그런데 이게 맞을까? 함수가 종료되면 result객체는 자동으로 소멸된다. 그래서 이 함수의 결과 값은 아무것도 없는 바이트덩어리일 뿐이다. 그래서 우리는 함수가 종료되어도 result객체가 자동 소멸되지 않도록 다음과 같이 작성할 수도 있다.

const MyNumber& operator* (const MyNumber& lhs, const MyNumber& rhs)
{
    MyNumber *result = new MyNumber(lhs.d * rhs.d);
    return *result;
}

물론 이 코드에서 이제 결과 값은 정상적으로 나온다. 그런데 여기서 할당한 메모리는 도대체 누가 해제해 주느냐는 말이다. 이렇게 참조자로 반환된 이상 이 메모리를 해제할 방법은 없다. 물론 이런 방법이 통하는 것도 있긴하다. 예를 들어 이런 프렌드 함수가 아니라 멤버함수로 자신의 클래스에 특정 객체를 생성하여 저장하는 식으로 할땐 말이다.

class Object { ... }

class Scene {
public:
    ...
    ~Scene()
    {
        for (auto iter : vec_Object) {
            delete *iter;
        }
    }
    
    const Object& createObject(/*매개변수 생략*/)
    {
        Object* pObj = new Object();
        vec_Object.push_back(pObj);
        return *pObj;
    }
    
private:
    std::vector<Object*> vec_Object;
}

만약 이런식으로 클래스 내부에서 동적할당한 녀석을 저장하고 추후에 해제할 수 있도록 설계해 놨다면 이 코드는 문제가 되지 않는다. 하지만 여기서 operator*는 굳이 메모리를 할당하여 저장해 놓을 이유가 없기 때문에 잘못된것이다.

 

그래서 동적할당한 메모리가 문제가 된다고 생각하여 프로그램 종료시에 자동으로 해제되는 데이터 영역을 이용하기로 생각했다고 치자.

const MyNumber& operator* (const MyNumber& lhs, const MyNumber& rhs)
{
    static MyNumber result;
    result = MyNumber(lhs.d * rhs.d);
    return result;
}

자, 이렇게 해놓고 보니 뭔가 잘 동작할것만 같다. 메모리 문제도 없어보이고, 하지만 아래 코드를 보고 나면 심각한 문제를 가지고 있다는 것을 알게된다.

bool operator==(const MyNumber& lhs, const MyNumber& rhs); // 비교연산자

MyNumber a, b, c, d;
...
if ((a * b) == (c * d)) {
    ...
} else {
    ...
}

자, 여기서 바로 문제가 발생한다 a * b, c * d 모두 operator*를 호출하는데 여기서 반환값은 모두 result라는 정적 객체를 공유하게 된다. 그러면 당연히 이 결과는 항상 자기 자신을 비교하는 꼴이 되므로 늘 같다고 나오게 된다. 자 이렇듯이 함수에서 객체를 반환해야 할 경우에는 아까 말한 경우가 아닌이상 제발 참조자를 반환하려고 들지 말자. 

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

그래서 위와 같이 바로 한 객체를 생성하여 반환하도록 하면 컴파일러에 따라 적절히 생성 소멸과정을 제거해 줄 수도 있기 때문에 반환을 할때는 값으로 반환하도록 하자.

반응형