항목 21 : 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자
유리수를 나타내는 클래스가 하나 있다고 가정합시다. 이 클래스에는 두 유리수를 곱하는 멤버 함수가 선언되어 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
class Rational {
public :
Rational(int numerator = 0, int denominator = 1);
...
private :
int n, d;
friend
const Rational operator*(const Rational& lhs, const Rational& rhs);
};
Colored by Color Scripter
|
여기서 operator*를 보면 이 함수가 참조자를 반환하도록 만들어졌다면, 이 함수가 반환하는 참조자는 반드시 이미 존재하는 Rational 객체의 참조자여야 합니다. 이 객체에는 곱셈이 가능한 두 객체의 곱이 들어 있어야 하는 것은 말할 필요도 없고요.
그럼 반환될 객체는 어디에 있을까요? 'operator* 호출 전에 어디엔가 생겼겠지'라고 생각하는 분은 혹시 없겠죠? 무슨 말인고 하니,
1
2
3
4
5
|
Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5
Rational c = a * b; // c는 3/10이어야 합니다.
|
위의 코드에서 10분의 3이란 값을 가진 유리수가 이미 생겨 주지 않을까 하는 기대를 걸면 난감하다는 이야기입니다. C++ 세상엔 '거저'가 없습니다. 그 유리수(객체)에 대한 참조자를 operator*에서 반환할 수 있으려면, 그 유리수 객체를 직접 생성해야 한다는 말입니다.
함수 수준에서 새로운 객체를 만드는 방법은 딱 두 가지뿐입니다. 하나는 스택에 만드는 것이고, 또 하나는 힙에 만드는 것입니다.
스택에 객체를 만들려면 지역 변수를 정의하면 됩니다.
1
2
3
4
5
|
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
Colored by Color Scripter
|
이런 방법은 피했으면 좋겠습니다. 생성자가 불리는 게 싫어서 시작한 일인데, 결국 result가 다른 객체처럼 생성되어야 합니다. 게다가 이 연산자 함수는 result에 대한 참조자를 반환하는데, result는 지역 객체입니다. 다시 말해 함수가 끝날 때 덩달아 소멸되는 객체입니다.
다음은 후자의 방법을 살펴볼 순서입니다. 함수가 반환할 객체를 힙에 생성해 뒀다가 그녀석의 참조자를 반환하는 것은 어떨까요?
1
2
3
4
5
|
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
Colored by Color Scripter
|
하지만 여전히 생성자가 한 번 호출되기는 매한가지입니다. new로 할당한 메모리를 초기화할 때 생성자가 호출되니 말입니다. 그러나 이것 말고 다른 문제가 하나 더 있습니다. 여기서 new로 생성한 객체를 누가 delete로 뒤처리해 주길 바란다는 말입니까?
1
2
3
|
Rational w, x, y, z;
w = x * y * z; // operator*(operator*(x, y), z)와 같습니다.
|
여기서는 한 문장 안에서 operator* 호출이 두 번 일어나고 있기 때문에, new에 짝을 맞추어 delete를 호출하는 작업도 두 번이 필요합니다. 그런데 operator*의 사용자 쪽에서는 이렇게 할 수 있는 합당한 방법이 없습니다. operator*로부터 반환되는 참조자 뒤에 숨겨진 포인터에 대해서는 사용자가 어떻게 접근할 방법이 없기 때문입니다.
Rational 객체를 정적 객체로 함수 안에 정의해 놓고 이것의 참조자를 반환하는 식으로 하면 어떨까요?
1
2
3
4
5
6
7
8
|
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result; // 반환할 참조자가 가리킬 정적 객체
result = ...; // lhs와 rhs를 곱하고 그 결과를 result에 저장합니다.
return result;
}
Colored by Color Scripter
|
정적 객체를 사용하는 설계가 항상 그러하듯, 이 코드 역시 스레드 안전성 문제가 얽혀 있습니다. 하지만 이보다 더 확실한 약점이 있는데, 아래에 준비한 멀쩡한 코드를 보며 고민해 봅시다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
bool operator==(const Rational& lhs, Rational& rhs);
Rational a, b, c, d;
...
if((a * b) == (c * d))
{
// 두 유리수 쌍의 곱이 서로 같으면 적절한 처리를 수행합니다.
}
else
{
// 다르면 적절한 처리를 수행합니다.
}
Colored by Color Scripter
|
((a * b) == (c * d)) 표현식이 항상 true 값을 냅니다.
operator== 함수가 호출될 때를 곰곰히 따져 봅시다. 이때 분명 두 개의 operator*함수 호출이 활성화되어 있을 것이고, 각각의 호출을 통해 operator* 안에 정의된 정적 Rational 객체의 참조자가 반환될 것입니다. operator==이 비교하는 피연산자는 operator* 안의 정적 Rational 객체의 값, 그리고 operator* 안의 정적 Rational 객체의 값입니다.
이정도면 operator* 등의 함수에서 참조자를 반환하는 것만큼 시간낭비가 없다는게 증명된 것 같습니다.
새로운 객체를 반환해야 하는 함수를 작성하는 방법에는 정도가 있습니다. 바로 '새로운 객체를 반환하게 만드는 것'이죠. 그러니까 Rational의 operator*는 아래처럼 혹은 아래와 비슷하게 작성해야 합니다.
1
2
3
4
|
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
Colored by Color Scripter
|
이 코드에도 반환 값을 생성하고 소멸시키는 비용이 들어 있지 않느냐는 분들이 있습니다.
맞습니다. 그러나 끝까지 따져 보면 여기에 들어가는 비용은 올바른 동작에 지불되는 작은 비용입니다. 모든 프로그래밍 언어가 그러하듯, C++에서도 다 컴파일러 구현자들이 가시적인 동작 변경을 가하지 않고도 기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 배려해 두었습니다. 그 결과, 몇몇 조건하에서는 이 최적화 메커니즘에 의해 operator*의 반환 값에 대한 생성과 소멸 동작이 안전하게 제거될 수 있습니다.
결론을 말하자면, 참조자를 반환할 것인가 아니면 객체를 반환할 것인가를 결정할 때, 이것만 잊지 말아주세요. 어떤 선택을 하든 올바른 동작이 이루어지도록 만드는 것! 이것이 진짜로 여러분이 할 일입니다.
꼭 잊지 말아야 할 것!
지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지마세요.
'언어 > C++' 카테고리의 다른 글
[Effective C++] 멤버 함수보다는 비멤버 비프렌드 함수와 더 까가워지자 (0) | 2020.01.23 |
---|---|
[Effective C++] 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2020.01.22 |
[Effective C++] '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다 (0) | 2020.01.20 |
[Effective C++] 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2020.01.19 |
[Effective C++] 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2020.01.18 |