참조자는 이름일 뿐이다. 존재하는 객체에 붙는 다른 이름이다. 즉, 참조자는 어떤 것에 대한 또 다른 이름이어야 하니 반드시 존재해야 한다(나는 이 성질을 이용하여 매개변수에 참조자를 쓰고 있다. 포인터와 다르게 NULL값이 들어가면 안되기에 편함).
함수 수준에서 새로운 객체를 만드는 방법은 두 가지뿐이다.
1.스택에서 생성(지역 변수)
2.힙에서 생성(동적 할당)
아래와 같은 클래스가 있다고 가정하자
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*는 곱셉 결과를 값으로 반환하도록 선언되어 있다.
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의 참조자는 이미 소멸된 메모리를 가르킨다.
operator*을 힙에서 생성하면
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational* result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d);
return *result;
}
여전히 생성자가 호출된다. 또한 동적할당을 했기에 result를 메모리 해제해줘야 한다. 근데 도대체 어디서 해제할 것인가?
생성자를 호출하지 않는 방법은 없는 것같으니 최소화 한다고 생각해보면 정적 객체가 떠오를 것이다.(처음 한 번만 생성되기 때문)
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;
}
전혀 생각하지 못한 문제점이다. static은 항상 같은 객체를 반환할 것이다. 위의 코드처럼 a * b와 c * d의 반환되는 Rational 참조자는 항상 같은 객체의 참조자가 반환될 것이다. 그러므모 항상 true가 반환될 것이다.
참조자를 사용하는게 성능에 좋다는 함정에 빠져 오류를 범할 수 있으니 적재적소에 써야지 남발하면 좋지 않다는 것이 이번 챕터의 결론이다. 비용을 지불하더라도 올바른 비용이라면 지불할만 하다.
참조자 반환으로는 에러를 범할 경우가 많기에 새로 만든 객체를 반환하게 만드는 방법이 올바른 비용이라 할 수 있다.
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
결국 이 코드도 생성자와 소멸자를 호출하고 있지만, 컴파일러 구현자들이 가시적인 동작 변경을 하지 않고도 기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 배려해 두었다. 그 결과 몇몇 조건하에서는 이 최적화 매커니즘에 의해 operator*의 반환 값에 대한 생성과 소멸 동작이 안전하게 제거될 수 있다. 이를 반환 값 최적화(return value optimization: RVO)(*주 1)라고 한다.
이것만은 잊지 말자
지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 힙에 할당된 객체에 대한 참조자를 반환하는 일, 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대로 하지말자.
(*주 1) 반환 값 최적화(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;
}
TEST a = function();를 읽으면서 function 함수에서 TEST의 생성자를 호출하고 생성한 객체를 반환하고 TEST a에 복사 생성자가 호출될거라 생각한다.
하지만 fucntion 함수로 생성하면 생성자만 호출되고 복사 생성자, 소멸자는 호출되지 않았다.
function2 함수는 Debug모드를 Release모드로 바꾸면 똑같이 반환 값 최적화가 적용된다.
'서적 정리 > Effective C++' 카테고리의 다른 글
25.예외를 던지지 않는 swap에 대한 지원도 생각해 보자 (0) | 2021.12.16 |
---|---|
24.타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2021.12.15 |
23.멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자. (0) | 2021.12.14 |
22.데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2021.12.14 |
20.'값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다 (0) | 2021.12.14 |
19.클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2021.12.14 |
18.인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2021.12.14 |
17.new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 (0) | 2021.12.11 |
댓글