[Effective C++] 항목 21 : 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자

수민이슈·2023년 3월 24일
0

Effective C++

목록 보기
21/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 지역 스택 객체, 힙에 할당된 객체, 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은
그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대 하지 말자!!


🖊️ 실제 있지도 않은 객체의 참조자를 반환하는 경우

대개 '값에 의한 전달' 보다 '상수 객체 참조자에 의한 전달'이 낫다.
그렇다고 모든 상황에서 참조에 의한 전달만을 고집하다가,
실제로 있지도 않은 객체의 참조자를 넘기게 될 수도 있다.

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1);
    ...
private:
	int n, d;

friend
	const Rational operator* (const Rational& lhs, const Rational& rhs);
};

이런.. 유리수를 나타내는 클래스가 있다고 치자

operator*는 곱셈의 결과를 값으로 반환하게 되어 있다.
아무런 문제가 읍다.

참조자는 존재하는 객체에 붙는 이름

참조자는 just 이름이다.
그냥 존재하는 객체에 대해서 붙는 또 다른 이름.
그래서
함수가 참조자를 반환한다면,
이 함수가 반환하는 참조자는 반드시 이미 존재하는 객체의 참조자이어야 한다!!
당연함.

Rational a(1, 2);
Rational b(3, 5);

Rational c = a * b;

이 상황에서
operator*가 반환하는 객체는
어디서 생성하냐?

그래서 새로운 객체를 만들어야만 한다.

새로운 객체를 반환해야 하는 함수 작성법

스택에 만들기 : 지역 변수 정의

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

그래 이렇게 했다고 치자
지역변수 result를 정의했으니까 생성자가 호출될거다
그래서 반환하는 result의 참조자는 유효할거다

근.데.
result는 지역 변수니까 함수가 끝나면 소멸될거다
그러면?
operator*가 반환한 result의 참조자는
이제는 더 이상 무효한 객체를 가리키고 있다는 거다
그러면.. 이게 말이 되냐?
안되겟징 당연히..

그래서 지역 객체에 대한 참조자를 반환하면 안된다.

힙에 만들기 : new로 동적 할당

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

이 경우에는,,
힙에서 new로 result를 생성했으니까 생성자가 호출될거다
(new로 할당한 메모리를 초기화할 때 생성자가 호출되니까)

근데 이거
누가 delete 해줌?

그래서 결국
메모리가 누출될 수 밖에 없당.

Rational w, x, y, z;
w = x * y * z;

이거는 사실 w = operator*(operator*(x, y), z);이랑 똑같다
그러면
operator* 호출을 2번 했으니까 delete도 2번 호출해줘야 하는데
operator*로부터 반환되는 참조자 뒤 포인터에는 접근할 수 있는 방법이 업슴..

그래서 어쩔 수가 없다.

정적 객체를 함수 안에 정의해놓기

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

어떤디.
근데 물론
정적 객체는 스레드 안정성 문제를 동반한다.

근데 또 문제가 있슴.

bool operator== (const Ratioanl& lhs, const Rational& rhs);

Rational a, b, c ,d;
...
if ((a * b) == (c * d)) { ... }
else { ... }

이런 상황이 있다고 쳐

이 코드랑 똑같은게

if (operator==(operator*(a,b),operator*(c,d));

이거.
그러면
operator==의 두 인자는
operator*에서 반환받은 정적 객체의 참조자
그러면
결국
둘 다 항상 똑같을거다.

이건 정적 배열을 쓰든 벡터를 쓰든 똑같을거다...

Good : 새로운 객체를 반환하자

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

물론 이 코드에도
반환 값을 생성, 소멸하는 비용이 필요하다
근데
이정도는 그냥 괜찮다

컴파일러 구현자들이, 가시적인 동작 변경을 하지 않고도 기존 코드의 수행 성능을 높이도록 최적화를 적용해준다
그래서 최적화 메커니즘에 의해 opeator* 반환값에 대한 생성, 소멸 동작이 안전하게 제거될 수 있당.


😊

참조자를 반환할지 객체를 반환할지 고민되면
올바른 동작이 되도록 하면 된당.

이미 존재하는 객체에 대한 참조자여야 한다라는거
알고있지만 좀 더 와닿았다

0개의 댓글