728x90
반응형

함수 수준에서 새로운 객체를 만드는 방법은 스택에 만들거나(지역변수), 힙에 만드는 것(동적 할당) 두 가지뿐이다.

 

함수에서 참조자를 반환할 때의 문제점

1. 스택

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

result는 다른 객체처럼 생성자를 통해 생성된다. 그리고 result는 지역 변수이므로 operator*가 끝난 시점에 소멸된다. 즉, result는 이미 소멸된 메모리를 가리킨다.

 

2. 힙

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

여전히 생성자 호출 비용이 발생하며, 동적 할당을 했기 때문에 result 메모리를 해제해줘야 한다. 하지만 해제하는 부분이 없다. 즉, 메모리 누수가 발생한다.

예를 들어 위와 같은 코드를 만든 상태에서 아래와 같은 함수를 호출했다고 가정하자.

void Func()
{
	Rational w, x, y, z;
	w = x * y * z; // operator*(operator*(x,y),z)
}

위의 x * y * z; 문장에서 operator*가 2번 호출되어 new를 2번 실행했으니 delete 또한 2번 해줘야 하는데, 위의 객체에 접근할 방법이 없기 때문에 delete를 해줄 수 없다.

 

3. 정적 객체

그럼 생성자 호출을 최소화한다고 생각해보면, 처음 한 번만 생성되는 정적 객체를 사용하면 되지 않을까 싶을 수도 있을 것이다.

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	static Rational result(lhs.n*rhs.n, lhs.d*rhs.d);
	return result;
}
bool operator==(const Rational& lhs, const Rational& rhs)
{
	Rational a, b, c, d;

	if ((a * b) == (c * d)) //operator*은 정적객체를 반환하기에 항상 같은 객체가 반환됨
		return true;
	else
		return false;
}

하지만 처음 한 번만 생성하기에 스레드 안정성 문제가 생기며 위의 코드처럼 operator== 연산자 오버 로딩 시 항상 같은 참조자를 반환하게 되기에 == 연산 시 항상 true를 반환하게 된다.

 

해결 방법

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

이 코드도 생성자와 소멸자를 호출하지만 올바른 동작에 지불되는 작은 비용으로 지불할만한 비용이다.

즉, 새로운 객체를 생성해서 리턴하는 게 가장 옳은 방법이다. 그리고 C++에서는 컴파일러 구현자들이 가시적인 동작 변경을 하지 않고도 기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 배려했다.

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

 

반환 값 최적화

C++11 이후 표준으로 제정되어, 컴파일러가 알아서 복사 없이 최적화한다.

class TEST
{
public:
	TEST() { cout << "생성자" << endl; }
	~TEST() { cout << "소멸자" << endl; }
	TEST(const TEST& test) { cout << "복사 생성자" << endl; }
};

TEST function() { return TEST(); }

TEST function2()
{
	TEST a;
	return a;
}

int main()
{
	cout << "function" << endl;
	TEST a(function());
	cout << endl;
    
	cout << "function2" << endl;
	TEST b(function2());
	cout << endl;

	system("Pause");
	return 0;
}

Debug모드 결과
Release모드 결과

 

요약

  • 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지 마라.(항목 4를 보면 지역 정적 객체에 대한 참조자를 반환하도록 설계된 올바른 코드 예제를 찾을 수 있다. 최소한 단일 스레드 환경에서는 통한다.)
728x90
반응형