주간 팁 #180: Dangling References(유효하지 않은 참조) 피하기
원래 게시일: 2020년 6월 11일
작성자: Titus Winters
최종 업데이트: 2020년 6월 11일
바로가기: abseil.io/tips/180
소개
C++는 다른 많은 언어와 달리, 유효하지 않은 메모리를 참조하는 것을 방지하는 안전 장치가 부족합니다. 이미 delete
된 객체의 포인터를 역참조하거나 스코프에서 벗어난 객체를 참조할 수 있습니다. 클래스 타입조차도 이러한 위험을 내포하고 있습니다. 특히, view
와 span
이라는 이름을 사용하는 객체는 “참조 의미론(reference semantics)을 가지며 dangling(유효하지 않음)이 발생할 수 있음”을 나타냅니다. 이러한 타입은 참조 의미론을 가지며, 항상 포인터가 가리키는 데이터를 소유하지 않는다는 점에서 주의가 필요합니다.
Dangling References와 C++ 이해하기
다른 언어에서 C++로 넘어온 사람들에게는 몇 가지 놀라운 점이 있습니다. C++의 타입 시스템은 다른 언어보다 훨씬 복잡하며, 참조, 임시 객체, 얕은 상수성(shallow const), 포인터, 객체의 수명 등을 세세히 이해해야 합니다. 특히, C++에서 포인터나 참조를 가지고 있다고 해서 객체가 여전히 존재한다는 보장이 없다는 사실을 아는 것이 중요합니다. C++은 가비지 컬렉션을 사용하지 않으며 참조 카운트도 없으므로, 객체의 핸들(handle)을 가지고 있다는 것만으로 객체가 살아있음을 보장하지 못합니다.
다음 예를 살펴보세요.
int* int_handle;
{
int foo = 42;
int_handle = &foo;
}
std::cout << *int_handle << "\n"; // 문제 발생
위 코드에서 int_handle
을 역참조하면, 수명이 끝난 객체를 참조하려는 시도가 됩니다. 이는 버그이며, 정의되지 않은 동작(undefined behavior)입니다. 놀랍게도, 정의되지 않은 동작의 결과 중 하나는 “의도한 대로 동작하는 것처럼 보이는 것”일 수 있습니다. 예를 들어, 42
가 출력될 수도 있습니다. 그러나 이는 제대로 동작하는 것처럼 보여도 여전히 버그입니다.
위 예제에서 두 가지 중요한 교훈을 얻을 수 있습니다:
- 다른 언어에서는 프로그램이 정상적으로 실행되거나 예상대로 작동하면 올바르게 작성되었다고 간주할 수 있습니다. 그러나 C++에서는 프로그램이 실행된다고 해서 올바르다고 보장할 수 없습니다. 이는 컴파일러가 우연히 원하는 결과를 내놓았을 뿐입니다.
- 포인터나 참조가 객체를 가리킨다고 해서 그 객체가 여전히 살아있고 유효하다는 보장이 없습니다. C++에서는 이러한 상황에 대한 런타임 검사나 자동 메모리 관리를 제공하지 않으므로, 객체의 수명이 보장되는지 확인하는 것은 프로그래머의 책임입니다.
클래스 타입도 위험하다
포인터와 참조뿐만 아니라, 일부 클래스 타입도 동일한 위험을 내포하고 있습니다. 예를 들어, 반복자(iterator)를 살펴보세요.
std::vector<int>::iterator int_handle;
{
std::vector<int> v = {42};
int_handle = v.begin();
}
std::cout << *int_handle << "\n"; // 문제 발생
이 경우도 이전 예제와 마찬가지로, 반복자가 벡터 내부의 객체(v[0]
)를 참조하고 있지만, 객체의 수명이 끝난 이후에 접근하려고 하므로 문제가 발생합니다.
C++에서는 유효하지 않은 포인터, 참조, 반복자를 사용하는 경우 무엇이 발생할지 정의하지 않습니다. 따라서 이러한 코드는 항상 잘못된 코드입니다.
Dangling이 발생할 수 있는 클래스 타입
string_view
와 같은 타입은 참조 의미론을 가지며, 데이터를 소유하지 않습니다. 이러한 핸들 타입을 사용할 때는 항상 데이터를 참조하는 객체의 수명을 명확히 보장해야 합니다.
다른 유사한 타입으로는 span<T>
가 있습니다. 이는 연속적인 데이터 버퍼를 나타냅니다. span<const T>
는 데이터를 수정하지 못하도록 보장하며, string_view
와 유사합니다.
이러한 핸들 타입은 함수 매개변수로 사용할 때는 안전하지만, 저장(storage) 목적으로 사용할 때는 주의가 필요합니다. 객체의 수명이 보장되지 않는 경우, 심각한 버그를 유발할 수 있습니다.
결론
C++에서 핸들 타입(string_view
, span
등)을 사용할 때는 참조 대상의 객체가 유효한지 확인하는 논리가 필요합니다. 특히 핸들 타입을 저장하거나 사용하려는 경우, 객체의 수명이 핸들보다 길다는 확신이 필요합니다.
추가로, C++20 span
및 Abseil Span
을 참조하여 더 자세한 정보를 확인하세요.