본문 바로가기
프로그래밍 언어/C, C++

[C++] 레퍼런스, const 레퍼런스 반환 / 레퍼런스와 임시 개체의 수명

by 니키티스 2024. 2. 5.

C++ Return value, reference, const reference

 

C++ Return value, reference, const reference

Can you explain to me the difference between returning value, reference to value, and const reference to value? Value: Vector2D operator += (const Vector2D& vector) { this->x += vector...

stackoverflow.com

레퍼런스를 반환하는 것, const 레퍼런스를 반환하면 무슨 일이 일어나는지 알아보자.

추가로 레퍼런스를 알아보는 김에 레퍼런스에 의해 임시 개체의 수명이 변화할 수 있다는 것도 알아볼 예정이다.

동기

참조(reference, &)를 함수의 인자로 사용하는 경우는 많이 봤지만 함수의 반환 타입이 reference인 경우는 언리얼 엔진을 공부하며 처음 발견하게 됐다.

// https://docs.unrealengine.com/5.1/ko/epic-cplusplus-coding-standard-for-unreal-engine/
// 나쁜 예 - const 배열 반환
const TArray<FString> GetSomeArray();

// 좋은 예 - const 배열로의 레퍼런스 반환
const TArray<FString>& GetSomeArray();

대체 함수가 reference를 반환하면 어떤 일이 발생하는 것일까?

또, 왜 const reference 같은 것을 사용할까?

reference와 const reference

reference를 반환할 때 생기는 일

값으로 반환할 때와 달리 reference로 반환하면, 포인터를 반환하는 것과 비슷한 일이 일어난다.

다만 reference의 특징상 다른 변수를 가리킬 수가 없다.

int& f();
...
int main()
{
    int& ref = f(); // 레퍼런스로 받을 수도 있고...
    int val = f(); // 그냥 값으로 받아도 된다.
}

reference는 개체 자체를 복사하지 않기 때문에 반환이 빠르다. 즉, 크기가 있는 클래스나 구조체를 반환할 때 적합하다.

또한 reference 변수의 값을 변경했을 때 원래 그 개체의 값도 함께 변경된다.

그러나 지역 변수를 reference로 반환하면 문제가 생긴다. 범위를 벗어나면서 값이 메모리 밖을 가리키기 때문이다.

int& wrong_func()
{
    int a = 10;
    return a; // 절대 이렇게 하면 안된다.
}

int& ref = wrong_func(); // 실제 전달될 땐 a가 범위를 벗어나 i엔 쓰레기 값이 전달될 수 있다.

또, 포인터를 참조로 반환해도 안된다. 포인터를 삭제하긴 해야 하는데, 이때 참조를 삭제하는 이상한 작업이 생기기 때문이다.

int& wrong_func2()
{
    int* ptr = new int;
    return *ptr; // 절대 이렇게 하면 안된다.
}

int& ref = wrong_func2(); // 포인터를 참조로 반환하면 작동은 되는데...
delete &ref; // 이렇게 지우려고 하면 명백히 이상하다

그래서 reference를 반환할 땐 함수보다 개체의 수명(lifetime)이 더 길 때만 해야 한다.

예를 들면 참조로 들어온 매개변수를 반환하거나, 매개변수로 들어온 배열의 원소를 반환하거나, 함수 내에서 소멸되지 않는 클래스, 구조체를 반환할 때 등이 있겠다.

// 참조로 들어온 매개변수를 반환할 때
int& GetElement(vector<int>& v, int index)
{
    return v[index];
}
// 매개변수로 들어온 배열의 원소를 반환할 때
int& GetParent(int tree[], int index)
{
    return tree[index / 2];
}
// 수명이 더 긴 클래스, 구조체를 반환할 때
class IntClass
{
public:
    IntClass(int i) : i_(i) {}

    int& get() const { return i_; }
private:
    int i_;
}

reference 타입을 사용하면 안 되는 경우에 대해서는 스택오버플로우의 글(Is the practice of returning a C++ reference variable evil? - Stack Overflow)에서 잘 정리되어 있으니 참고하면 좋을 듯하다.

const reference를 반환할 때 생기는 일

const reference는 reference와 대부분 같지만 참조된 값을 변경할 수 없다.

대부분은 reference만 사용해도 괜찮지만, 잘못된 reference 활용을 막기 위해서 const reference를 사용해야 할 때가 있다.

// 일반 reference
Vector2D& operator += (const Vector2D& vector)
{
    this->x += vector.x;
    this->y += vector.y;
    return *this;
}
// const reference
const Vector2D& operator += (const Vector2D& vector)
{
    this->x += vector.x;
    this->y += vector.y;
    return *this;
}

예를 들어 위와 같이 Vector2D 에 대해 + 연산을 오버라이드한다고 치자.

이때 (v1+=v2)=v3; 같이 이상하게 할당 연산을 한다고 치자.

첫 번째 Vector2D&를 반환하는 함수에서는 (v1+=v2)의 결과가 v1이 되어서 v1=v3가 실행된다.

반면 const Vector2D&를 반환하는 함수에서는 (v1+=v2)의 결과가 const Vector2D& 이므로 v1v3을 할당하려 하면 에러가 발생하면서 실패하게 된다.

이렇듯, const reference는 reference를 의도치 않은 방향으로 활용하는 일을 막는 역할을 한다.

언제 const reference를 반환하면 좋을지에 관해서는 다음 스택오버플로우의 글(oop - When is it a good idea to return a const reference in C++? - Stack Overflow)에서 잘 정리되어 있으니 참고하자.

지역 변수를 reference로 전달하고 싶을 때

클래스나 구조체의 경우 함수에서 값을 반환하는 것보다 reference, const reference를 반환하는 것이 빠르다.

그러나 지역 변수는 reference로 반환하면 변수의 범위(scope)를 벗어나서 이상한 값을 가리키게 된다. 이 경우에는 좋은 방법이 있을까?

임시 개체

C++ 표준에 따르면 함수의 반환값은 기본적으로 임시 개체(temporary instance)로 저장된다.

int f()
{
    int value = 10;
    return value; // 10이라는 값을 반환한다.
}

int main()
{
    int a = f();
    // f()의 결과가 임시 변수에 저장되고,
    // 임시 변수의 값이 a로 전달된다.
    // 결과적으로 a = 10;이 된다.

    // f() = 20;
    // 에러 발생: f()의 결과가 저장된 임시 변수는
    // rvalue에 해당하므로 값을 별도로 저장할 수 없다.
}

함수에서 지역변수를 반환한다면 함수를 빠져나올 때 그 메모리는 해제되기 때문에, 함수 밖으로 값을 전달하려면 임시 개체가 필요하다.

물론 꽤 많은 컴파일러들이 반환값 최적화(Return value optimization, RVO)라 해서 많은 경우 임시 개체를 사용하지 않도록 최적화를 한다.

하지만 제대로 최적화를 수행하지 못하는 경우도 존재한다.

Temporary lifetime extension

임시 개체는 함수 밖의 변수에 값을 전달하는 것이 목적이다.

따라서 임시 개체는 함수의 반환 값을 필요한 곳에 전달하고 나면 소멸한다.

하지만 여기에는 예외가 있는데, 이 임시 개체를 reference 변수로 참조하면 임시 개체가 사라지지 않고 계속 유지된다는 것이다.

Whenever a reference is bound to a temporary object or to a subobject thereof, the lifetime of the temporary object is extended to match the lifetime of the reference.
참조가 임시 개체나 그 하위 객체에 묶여 있다면, 임시 개체의 수명(lifetime)은 참조 값이 사라질 때까지 늘어난다.
(Reference initialization - cppreference.com)

reference 변수가 임시 개체를 가리키게 된다면, 그 reference 변수가 사라지기 전까지 임시 개체도 사라지지 않는다는 것이다.

int f()
{
    int value = 10;
    return value;
}

int main()
{
    int& ref = f();
    const int& const_ref = f();

    cout << ref << ", " << const_ref << endl; // f()의 결과가 사라지지 않고 계속 유지된다.
    // 출력: 10, 10
}

만약 지역 변수를 반환한다면, 위와 같이 함수는 값을 반환하도록 하고 함수의 반환 값을 참조형 변수(&)로 가리키게 할 수도 있다.

이 외에도 임시 개체가 사라지지 않는 예외 케이스가 두 가지 더 있는데 궁금한 분들은 해당 링크에서 ‘There are the following exceptions from that’로 시작하는 부분을 읽어보면 좋을 듯하다(https://en.cppreference.com/w/cpp/language/lifetime#Temporary_object_lifetime).

 

추가

C++ Core Guidelines (isocpp.github.io)

만약 수명 주기에 관심이 있다면 읽어보자. C++의 창시자 비야네 스트롭스트룹(Bjarne Stroustrup)과 C++ 전문가 허브 서터(herb sutter)가 작성한 가이드라인이라고 한다.

참고 링크

C++ Return value, reference, const reference - Stack Overflow

Const Reference (tistory.com)

소년코딩 - C++ 08.05 - 값, 참조 및 주소로 값 반환 (Returning values by value, reference and address) (tistory.com)

Is the practice of returning a C++ reference variable evil? - Stack Overflow

c++ - Temporary lifetime extension - Stack Overflow

Lifetime - cppreference.com

댓글