관리 메뉴

기억을 위한 기록들

[CPP-effective] 4-4 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려 하지말자. 본문

C & CPP/Effective C++

[CPP-effective] 4-4 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려 하지말자.

에드윈H 2021. 4. 13. 17:52

이전에 작성한 글을 쓰고, 기존의 코드에 멀쩡하게 들어 있는 '값에 의한 전달' 부분을 '참조에 의한 전달'로 무작정 바꾸려고 하면 안 된다. 실제로 있지도 않은 객체의 참조자를 넘길 수도 있기 때문이다.

 

예를 들어, 어떤 유리수를 나타내는 클래스가 있다고 치자.

class Rational{
pulbic:
     Rational(int num = 0, int denominator = 1);
     //...
     
private:
     int n, d;
     
     friend const Rational operator*(const Rational* lhs, const Rational* rhs);    
};

 

해당 클래스의 operator*는 곱셉 결과를 값으로 반환하도록 되어있다. 값이 아닌 참조자를 반환할 수 있으면 비용 부담은 확실히 없을 것이다.

     const Rational& operator*(const Rational* lhs, const Rational* rhs); //값 반환에서 참조반환으로

 

하지만 참조자에 대해 생각해보자... 참조자는 그냥 이름이다. 이미 존재하는 객채에 붙는 다른 이름이다.

operator* 를 다시보면 이 함수가 참조자를 반환하도록 만들어졌다면, 반드시 이미 존재하는 Rational 객체의 참조자 여야 한다.

 

그럼 반환될 객체는 어디에 있을까? 단순히 operator 호출 전에 어디선가 생기겠지라고 생각하면 안된다.

operator*에서 반환할 수 있으려면, 객체를 직접 생성해야 한다.

 

함수 수준에서 새로운 객체를 만드는 방법은 딱 두 가지이다.

하나는 스택에 만드는 것, 또 하나는 에 만드는 것이다. 스택으로 만들면? 당연히 안된다.

const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //스택에 할당해버리면
    return result;  //return 되며 소멸된다..!
}

스택에 만드는 것은 안되고, 후자 방법으로 힙 기반으로 구현해보면

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

가능은 해진다. 하지만 new로 할당한 메모리를 초기화할 때 생성자가 호출된다.그리고 더 문제는 new로 만든 객체는 누가 delete 해줘야 하는 것인가?라는 점이다.

 

리턴 받은 객체가 잘 해제해준다면 문제가 없을지 모르더라도(스마트포인터나) 아래와 같은 상황이라면 이야기가 달라진다.

Rational w, x, y, z;

w = x * y * z; //operator*(operator*(x,y,),z); 와 같다.

한 문장에 operator* 호출이 두 번 일어나기에 delete도 2번 해주어야 한다는 점이다.

하지만.... 반환되는 참조자 뒤에 숨겨진 포인터에 대해 어떻게 접근할 방법이 없기 때문이다.

 

스택 기반이든, 힙 기반이든 operator*에서 반환되는 결과는 반드시 생성자를 꼭 한번 호출해야 한다. 필요 없는 생성자 호출을 피해보자. 

 

더 최악은

const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
    static Rational result; //static이라니...
    
    resutl = //..할당 해준다.
    return result; 
}

정적 객체(static)라니.... 아래와 같은 이유에 이 조차도 안된다.

Rational a,b,c,d;


if((a * b)==(c * d))
{
//...
}

이렇게 되면 같은 정적 객체를 가지고 있으니 무조건 if는 참이 된다. 불가능하다.

 

 

최선의 방법을 드디어 방법은 바로 '새로운 객체를 반환하게 만드는 것'이다.

const Rational operator* (const Rational& lhs, const Rational& rhs)
{
   return Rational(lhs.n * rhs.n, lhs.d * rhs.d); 
}

위 코드도 생성과 소멸의 비용이 들지 않냐고 물어본다면 맞다. 들어간다. 하지만 들어가는 비용은 올바른 동작에 지불되는 작은 비용이다. 게다가 컴파일러 구현자들이 가시적인 동작 변경을 가하지 않고도 기존 코드의 수행성능을 높이는 최적화를 적용할 수 있도록 배려해두었다.

 

그 결과 몇몇 조건하에서 이 최적화 메커니즘에 의해 operator*의 반환 값에 대한 생성과 소멸 동작이 안전하게 제거될 수 있다.(반환 값 최적화(return value optimization) RVO라고도 한다.)

hyo-ue4study.tistory.com/346

 

[CPP]반환 값 최적화(return value optimization)RVO란?

이펙티브 c++를 작성하다가 등장하여 관련하여 찾아보았다. 예제로 살펴보면 class T { public: T(int a) :num(a) { cout << "생성" << endl; } T(const T& ref) : num(ref.num) { cout << "복사" << endl; } ~T()..

hyo-ue4study.tistory.com

참조자를 반환할 것인가 아니면 객체를 반환할 것인가를 결정할 때엔, 어떤 선택을 하든 올바른 동작이 이루어지도록 만드는 것이다.!

 

정리

* 지역 스택 객채에 대한 포인터나 참조자를 반환하는 일. 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일. 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지 마세요.