RTTI? RAII?
RTTI (Run-Time Type Information)
런타임에 객체의 실제 타입을 파악할 수 있도록 하는 C++ 매커니즘
컴파일 시간이 아닌 ‘실행 중’(런타임)에 타입을 식별할 수 있게 해준다
RTTI 지원 기능
- dynamic_cast
- 다만 아래의 VTable이 추가로 필요
- 그렇기에 virtual 키워드가 들어간 가상함수를 포함한 클래스여야 제대로 동작
- VTable이 없다면 업캐스팅 같은 VTable을 사용하지 않는 경우만 제대로 동작
(캐스팅 관련하여 지난번에 다룬 포스팅)
- 다만 아래의 VTable이 추가로 필요
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // OK, 다만 VTable이 없다면 '다형 형식'이 아니라는 컴파일 에러(C2683)
- typeid 연산자
- C++에서 객체의 실제 타입을 반환함
(아래의 type_info 반환) - typeid(TypeName)인 경우는 컴파일 시간에 판단
- typeid(*ptr)인 경우는 런타임 중 판단
(VTable + Virtual 함수이 없다면 제대로 동작하지 않음)
- 정확히는 ‘정적 타입’만을 체크 (연산 자체는 런타임 이긴 함)
- nullptr 같은 것을 넣으면 bad_typeid 예외 발생
- 정확히는 ‘정적 타입’만을 체크 (연산 자체는 런타임 이긴 함)
- C++에서 객체의 실제 타입을 반환함
- std::type_info
- RTTI 정보를 저장하는 타입
- 전역적으로 하나의 type_info 개체를 생성하기에 == 연산 비교가 매우 빠름
- RTTI 옵션을 키는 경우, VTable에 type_info 정보가 추가로 저장된다
- RTTI 정보를 저장하는 타입
#include <typeinfo>
Base* b = new Derived();
const std::type_info& info = typeid(*b);
std::cout << info.name() << '\n';
VTable(가상 함수 테이블) 과의 연관성
-
VTable 자체는 ‘다형성’을 지원하기 위한 기능으로
실제 객체가 호출할 ‘함수 주소’를 찾기 위한 테이블 -
RTTI 옵션을 키면 VTable에 RTTI 용 typeinfo 포인터가 추가
(옵션을 끄면 RTTI에서 typeinfo 블록을 가리키는 항목 제거) -
그렇기에 RTTI 옵션을 키더라도
VTable + Virtual 키워드를 통한 가상함수를 포함해야
제대로 동작하므로 유의하자- VTable을 사용할 수 없으면 ‘정적 타입’ 검사만 가능해짐
- VTable을 사용할 수 없으면 ‘정적 타입’ 검사만 가능해짐
[vtable]
├─ entry#0 → &typeinfo for Derived
├─ entry#1 → &Derived::FuncA
├─ entry#2 → &Derived::FuncB
- 정적 타입 검사?
- 컴파일 타임에 선언된 ‘타입’을 기준으로만 판단함
(코드에 적힌 타입 자체를 기준으로 판단)
(업캐스팅의 경우, 컴파일 타임에 충분히 검사가 가능)
(그러나 다운 캐스팅의 경우는 타입 확인이 필요하기에 x)
- 컴파일 타임에 선언된 ‘타입’을 기준으로만 판단함
// 업 캐스팅 - 자식 클래스의 객체(포인터)를 부모 클래스 타입으로 가리킴
Base* b = new Derived();
// 다운 캐스팅 - 부모 클래스 타입의 포인터(객체)를 자식 클래스 타입으로 가리킴
Drived* d = dynamic_cast<Drived*>(b);
RAII (Resource Acquisition Is Initialization)
객체가 생성될 때, 자원을 획득하고(Initialization)
객체가 스코프를 벗어나며 소멸될 때, 자원을 반환(Resource Release)하는 패턴
자원의 소유권을 ‘객체’에 귀속시킨다
-
생성자에서 자원을 얻고
소멸자에서 자원을 반환
(메모리/리소스의 생명 주기를 ‘객체’의 생명주기에 묶어 자동으로 관리) -
메모리, 파일 핸들 등 다양한 ‘자원’을 획득한 후
해제를 잊어버리면 치명적인 문제가 발생하기에 이를 예방하기 위함 - C++에서 ‘변수’가 스코프를 벗어나면 ‘소멸자’를 자동으로 호출하는 것이 이를 위한 것
- 예외가 발생하여도 소멸자 호출
- 생성자/소멸자, 스코프 기반 수명, 예외 안정성, 소유권 개념 을 바탕으로 한다
- 예외가 발생하여도 소멸자 호출
- 이에 적합한 개념이 바로 ‘
스마트 포인터‘
스마트 포인터
일반 포인터처럼 동작하나,
메모리를 자동으로 반환하는 클래스 템플릿
- RAII 패턴에 기반하여 ‘스코프’를 벗어남에 기반하여 작동한다
- 일반 Row Pointer의 메모리 누수, 예외 안정성, 다중 포인터의 이중 삭제 등을 예방 가능
- ‘소유권’ 개념에도 적합한 기능
(메모리 해제 책임)
Unique_ptr (유니크 포인터)
-
한 객체의 소유자는 ‘하나’라는 개념
(단일 소유) -
소유권의 ‘이동’(Move)는 가능하나 ‘복사’는 불가능
(Move와 rvalue에 대하여?) -
Scope를 벗어나면 Delete됨
특징
- 가장 안전하고 빠름(다른 스마트 포인터들은 몇가지 확인 등이 필요하므로)
- 명확한 소유권 (독점적인 소유)
- 정리 개념이 확실 (Shared_ptr의 RefCount 개념이 없음)
std::unique_ptr<Foo> p = std::make_unique<Foo>(); // Scope 벗어날시 메모리 해제
Shared_ptr
-
한 객체를 ‘여럿’이서 소유
(공동 소유) -
별도의 제어 블록을 가지며
이곳에 ‘참조 카운트’를 저장
(추가적인 메모리 공간) -
참조될때마다 참조 카운트가 증가하며
참조하는 대상이 사라질때 참조 카운트 제거- 0이 되었을 때, 자원을 회수함
- 0이 되었을 때, 자원을 회수함
auto sp1 = std::make_shared<Foo>();
auto sp2 = sp1; // 같은 객체 공유 (참조 카운터 + 1)
특징
- 강한 참조(Shared_ptr)와 약한 참조(Weak_Ptr)이 나뉘며
강한 ‘참조’만 참조 카운트에 포함 - 일반 포인터 / Unique_ptr 보단 느림
(그렇게 신경쓸 정돈 아니지만 알아는 두기) - 순환 참조의 문제 존재
- 각각의 객체가 서로를 Shared_ptr로 가리키면
‘강한 참조’가 계속 1인 상태로 남아 있어
메모리 해제가 되지 않는 현상
- 각각의 객체가 서로를 Shared_ptr로 가리키면
// TSharedPtr<> 은 Unreal 의 Shared_ptr
class Parent
{
public:
TSharedPtr<Child> MyChild; // 부모 → 자식 (강한 참조)
};
class Child
{
public:
TSharedPtr<Parent> MyParent; // 자식 → 부모 (강한 참조)
};
void CreateCircularReference()
{
TSharedPtr<Parent> parent = MakeShared<Parent>();
TSharedPtr<Child> child = MakeShared<Child>();
parent->MyChild = child; // 자식 참조 카운트 +1
child->MyParent = parent; // 부모 참조 카운트 +1
}
Weak_ptr
- Shared_ptr을 ‘관찰’하지만 ‘소유’하진 않음
(비소유)
특징
- ‘약한’ 참조 카운트를 증가시키기에
‘순환 참조’를 일으키지 않는다
- Shared_ptr의 강한 참조 카운트가 0이 된 순간 메모리 해제
- 그렇기에 Weak_Ptr은 ‘Shared_Ptr’이 아직 존재하는지를
체크하고 사용하는 것이 권장됨
(C++ : lock, Unreal : Pin)
- Shared_ptr의 강한 참조 카운트가 0이 된 순간 메모리 해제
// AsShared()를 사용하려면 TSharedFromThis를 상속받아야 함
class UIWidget : public TSharedFromThis<UIWidget>
{
private:
TWeakPtr<UIWidget> ParentWidget; // 자식 → 부모 : 약한 참조 ✅
TArray<TSharedPtr<UIWidget>> Children; // 부모 → 자식 : 강한 참조 유지
public:
void NotifyParent(FString Message)
{
// 1. 부모가 아직 살아있는지 확인
TSharedPtr<UIWidget> Parent = ParentWidget.Pin();
if (Parent.IsValid())
{
// 2. Pin 성공 → 부모 안전하게 접근
Parent->ReceiveMessage(Message);
UE_LOG(LogTemp, Log, TEXT("부모에게 메시지 전달: %s"), *Message);
}
else
{
// 3. 부모가 이미 삭제됨
UE_LOG(LogTemp, Warning, TEXT("부모가 없어 메시지 전달 실패"));
}
}
void AddChild(TSharedPtr<UIWidget> Child)
{
Children.Add(Child); // 부모 → 자식 : 강한 참조
Child->ParentWidget = AsShared(); // 자식 → 부모 : 약한 참조 (TWeakPtr에 대입하면 자동 변환)
// ✅ 순환 참조 없음! 부모 삭제 시 자식도 자연스럽게 정리
}
};
스마트 포인터 정리 표
| 종류 | 소유권 | 특징 | 장점 | 단점 | 대표 사용 |
|---|---|---|---|---|---|
| unique_ptr | 단독 소유 | 복사 불가, 이동만 가능 | 빠르고 안전, delete 자동 | 공유 불가 | 단일 소유 리소스 |
| shared_ptr | 공유 소유 | 참조 카운트 기반 | 여러 곳에서 사용 가능 | 성능 저하, 순환 참조 위험 | 공동 소유 객체 |
| weak_ptr | 비소유 | shared_ptr 감시 | 순환 참조 해결 | lock 필요, nullptr 가능성 | shared_ptr 보조 참조 |
TMI - 정적 & 동적 바인딩
| 바인딩 종류 | 결정 시점 | 사용됨 | 대표 예 |
|---|---|---|---|
| 정적 바인딩 (Static Binding) | 컴파일 타임 | 일반 함수, 정적 함수, 비가상 함수 | 오버로딩, non-virtual 함수 |
| 동적 바인딩 (Dynamic Binding) | 런타임 | virtual 함수 | 가상 함수 override 호출 |
- 컴파일 타임의 비 가상함수, inline, 오버로딩 등이 정적 바인딩
- Virtual 키워드 + VTable이 대표적인 동적 바인딩
댓글남기기