1. 개요
우리는 항목 20에서 값에 의한 전달의 효율 문제에 대해서 공부했습니다.
그래서 우리는 기존 코드에서 값에 의한 전달이 있는 경우에 그것을 참지못하는 경우가 생기게 됩니다.
그러나 이것은 잘못된 마음가짐입니다.
이러다보면 실제로 있지도 않은 객체의 참조자를 넘기는 불상사를 만들어 내기도 합니다.
이러한 부분에 대해 자세히 설명해보고자 합니다.
2. 참조를 반환하는 것이 정답일까?
2-1. 참조를 반환 시키는 법
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
private:
int n, d;
friend
const Rational operator*(const Rational& lhs, const Rational& rhs);
};
여기서 * 연산자는 '값'을 반환하고 있습니다.
우리는 항목 20을 읽었기에 이 부분이 불편할지도 모릅니다.
Rational 객체를 생성하고 소멸하는 부분이 포함되어 있기 때문입니다.
그런데 이러한 비용이 과연 줄여야 하고 필요없는 비용일까요?
물론 참조를 반환한다면 이러한 비용을 줄일 수 있을 것입니다.
그런데 참조자는 그저 이름일 뿐입니다.
만약 이 함수가 참조를 반환하도록 만들어졌다면, 반환하는 참조자는 이미 존재하는 객체여야 합니다.
그럼 그 객체는 도대체 어디에 있을까요?
2-2. 참조 반환을 위한 객체 생성법
2-2-1. 스택에 생성
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
이런식으로 만드는것은 무슨일이 있어도 피해야합니다.
참조자를 반환하는데 result객체는 지역 객체입니다.
즉 스코프를 벗어나면 소멸되는 객체입니다.
소멸된 객체의 참조를 받아서 어떠한 행동을 하는 것은 정말로 위험한일이고 있어서도 안되는 일입니다.
이러한 코드를 절대로 작성하지 않도록 주의합시다.
2-2-2. 힙에 생성
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
이 코드 역시 문제가 많은 코드입니다.
new로 할당한 메모리는 도대체 누가 delete로 처리해주는 것일까요?
이러한 코드는 아무리 애를 써도 메모리 누수를 막는데 한계가 존재합니다.
Rational w, x, y, z;
w = x * y * z;
이런식으로 *를 두 번 호출하게 되면 new가 두 번 실행되게 됩니다.
그에 따라서 delete로 똑같이 2번 호출해주어야 하는 필요가 생깁니다.
그러나 사용자는 반환되는 참조자 뒤에 있는 포인터에 대해서 접근할 방법이 없습니다.
무슨일이 있어도 메모리 누수가 일어나는 코드입니다.
사용자가 delete를 호출할 수 없는 이유:
- delete 연산자는 포인터를 인자로 받습니다. (예: delete ptr;)
- 하지만 operator*를 호출한 사용자는 참조자만 가지고 있습니다.
3. 생성자 조차 호출되지 않게 해보기
2번에서 사용한 방법들은 전부 생성자를 호출하게 됩니다.
그러나 우리가 참조를 반환하고 싶어하는 이유는 생성자가 호출되는 비용을 줄이기 위해서 입니다.
Rational 객체를 static 객체로 함수안에 정의하고 참조를 반환하도록 만들어버리면 됩니다.
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = ... ; // lhs, rhs를 곱해서 result에 저장합니다.
return result;
}
이 코드에는 스레드 안정성 문제가 기본적으로 들어가 있고 그보다 더 큰 약점도 존재합니다.
- static 지역 변수 result는 이 operator* 함수를 호출하는 모든 스레드에 의해 공유됩니다. 즉, 여러 스레드가 동시에 이 함수에 진입하면, 그들은 모두 동일한 result 객체 인스턴스에 접근하게 됩니다.
만약 아래처럼 코드를 짠다고 생각해보겠습니다.
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
if ((a * b) == (c * d))
{
//do whatever’s appropriate when the products are equal;
}
else
{
//do whatever’s appropriate when they’re not;
}
결론부터 말하면 if문안의 수식은 항상 true를 반환합니다.
사실 생각해보면 당연합니다.
모두 같은 static객체를 참조하기 때문입니다.
즉 이러한 코드는 잘못된 코드인 것이죠
배열이나 벡터를 써서 교묘하게 이러한 문제를 피해가는 방법도 물론 가능하긴하지만
이렇게 되면 오히려 최적화가 아니라 부적화가 됩니다. (optimization > pessimization)
4. 그렇다면 어떻게 해야하나
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
그냥 새로운 객체를 만들어서 반환하게 만들면 됩니다.
물론 이렇게 되면 객체를 생성하는 비용이 들게 됩니다.
그러나 이것은 올바른 동작에 사용되는 아주 적은 비용일 뿐입니다.
이것만은 잊지 말자!
- 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두개 이상 필요해질 가능성이 있다면 절대로 하지 마세요(항목 4를 보시면 지역 정적 객체에 대한 참조자를 반환하도록 설계된 올바른 코드 예제를 찾을 수 있습니다. 최소한 단일 스레드 환경에서는 통합니다.)
'Effective C++ > Chapter 4: 설계 및 선언' 카테고리의 다른 글
[Effective C++] 항목 22: 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2025.05.30 |
---|---|
[Effective C++] 항목 20: '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다 (0) | 2025.05.29 |
[Effective C++] 항목 19: 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2025.05.18 |
[Effective C++] 항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2025.05.17 |