자, 제목그대로 이번 항목은 함수에서 객체를 반환해야 할경우 참조자를 반환하려 하지 말라는 것이다. 아래 코드를 보도록 하자.
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);
}
그래서 위와 같이 바로 한 객체를 생성하여 반환하도록 하면 컴파일러에 따라 적절히 생성 소멸과정을 제거해 줄 수도 있기 때문에 반환을 할때는 값으로 반환하도록 하자.
'C++ > Effective C++' 카테고리의 다른 글
항목 23 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2024.01.21 |
---|---|
항목 22 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (1) | 2024.01.21 |
항목 20 '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달'방식을 택하는 편이 대개 낫다 (2) | 2024.01.20 |
항목 19 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2024.01.20 |
항목 18 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2024.01.20 |