포스트

CS — rtti raii

답변 흐름 — RAII → 스마트 포인터 → virtual 소멸자 → vtable 꼬리질문 연결 다리

CS — rtti raii

내일 모의면접 주제는 “RTTI와 RAII에 대해서 설명해 주세요”다. 두 개념을 따로 외우기보다, RAII → 스마트 포인터 → virtual 소멸자 → vtable로 이어지는 꼬리질문 경로를 하나의 다리로 잡고 준비했다. 어제 vtable을 깊게 판 게 있어서, 어떤 꼬리질문이 와도 결국 vtable로 돌아오게 답변을 설계하는 게 목표다.


모의면접 답변

RTTI(Runtime Type Information)는 C++이 런타임에 객체의 실제 타입 정보를 얻을 수 있게 하는 메커니즘입니다. virtual 함수가 하나라도 있는 클래스는 vtable을 가지며, vtable 헤더에 type_info 포인터가 자동 삽입됩니다. typeid(*ptr)는 vptr을 통해 이 type_info에 접근해 실제 타입 이름을 반환하고, dynamic_cast는 RTTI를 이용해 다운캐스트를 시도한 뒤 실패하면 nullptr(포인터) 또는 std::bad_cast 예외(참조)를 반환해 안전하게 처리합니다. static_cast는 런타임 검사 없이 강제 변환하므로 타입이 틀리면 미정의 동작(UB)이 발생합니다.

RAII(Resource Acquisition Is Initialization)는 자원의 수명을 객체의 수명에 묶는 C++ 관용구입니다. 생성자에서 자원을 획득하고 소멸자에서 해제하면, 예외 발생 시 스택 해제(stack unwinding) 과정에서 소멸자가 자동 호출되어 누수 없이 정리됩니다. unique_ptr, shared_ptr, lock_guard, weak_ptr, ifstream이 모두 RAII 도구입니다. 단, 기반 클래스 포인터로 파생 객체를 delete할 때 virtual 소멸자가 없으면 파생 소멸자가 호출되지 않아 자원 누수가 발생하므로, 다형적 기반 클래스에는 반드시 virtual ~Base()를 선언해야 합니다.

핵심 개념

답변에 등장하는 용어를 면접 직전에 훑을 수 있게 목록으로 정리해 둔다.

  • RTTI (Runtime Type Information) — 런타임에 객체의 실제 타입을 확인하는 메커니즘. virtual 함수가 있는 클래스의 vtable 헤더에 type_info* 포인터가 자동 생성됨
  • typeid — 타입 정보를 반환하는 연산자. typeid(*ptr)은 vptr 경유 동적 타입(런타임), typeid(ptr)은 정적 타입(컴파일 타임) 반환
  • dynamic_cast — RTTI를 사용해 런타임 안전 다운캐스트. 실패 시 포인터 → nullptr, 참조 → std::bad_cast 예외
  • static_cast — 컴파일 타임 타입 변환. 런타임 검사 없음 — 잘못된 캐스트 시 Undefined Behavior
  • type_info — RTTI 데이터를 담는 구조체. vtable[-1] 위치에 포인터로 저장. name() 메서드로 타입 이름 반환
  • -fno-rtti — GCC/Clang에서 RTTI를 비활성화하는 컴파일 플래그. dynamic_cast/typeid 사용 불가. 성능 중심 환경(게임 엔진 등)에서 활용
  • RAII (Resource Acquisition Is Initialization) — 자원 수명을 객체 수명에 묶는 C++ 관용구. 생성자에서 획득, 소멸자에서 해제
  • 스택 해제 (Stack Unwinding) — 예외 발생 또는 함수 종료 시 스택 프레임이 역순으로 소멸되면서 소멸자가 자동 호출되는 과정
  • 예외 안전성 (Exception Safety) — 예외 발생 경로에서도 자원이 누수 없이 해제되는 속성. RAII로 보장
  • unique_ptr — 단독 소유권 스마트 포인터. 복사 불가, move만 허용. 런타임 오버헤드 없음
  • shared_ptr — 참조 카운팅 공유 소유권 스마트 포인터. 카운트가 0이 되면 자동 delete
  • weak_ptr — 소유권 없는 약한 참조. shared_ptr 순환 참조 문제 해결에 사용
  • lock_guard — 뮤텍스를 RAII로 감싼 래퍼. 생성자에서 lock(), 소멸자에서 unlock(). 예외 발생 시 데드락 방지
  • ifstream — 파일 핸들을 RAII로 관리하는 파일 스트림. 생성자에서 파일 열기, 소멸자에서 자동 fclose. 예외 발생 시 파일 핸들 누수 방지
  • virtual 소멸자 — RAII + 다형성 조합의 필수 조건. 기반 포인터 delete 시 파생 소멸자까지 올바르게 체인 호출됨

목차

  1. 핵심 요약 카드
  2. RTTI — Runtime Type Information
  3. RAII — Resource Acquisition Is Initialization
  4. 꼬리질문 예상 경로
  5. 언리얼에서의 RTTI / RAII

1. 핵심 요약 카드

RTTI 30초

1
2
3
4
5
RTTI = 런타임 타입 정보 시스템
메커니즘: vtable 헤더의 type_info* 포인터
도구: typeid (타입 이름), dynamic_cast (안전 캐스팅)
비용: vtable 탐색 오버헤드 → 성능 중심 환경에서 -fno-rtti
언리얼: Cast<T>(), IsA<T>(), GetClass() (UClass 기반)

RAII 30초

1
2
3
4
5
RAII = 자원 수명 = 객체 수명
생성자에서 획득, 소멸자에서 해제 → 예외 안전성 보장
도구: unique_ptr, shared_ptr, lock_guard, ifstream
언리얼: FScopeLock, TGuardValue, FString/TArray
함정: 기반 클래스 포인터로 delete 시 virtual 소멸자 필수

꼬리질문 연결 맵

1
2
3
4
5
6
7
8
9
10
11
12
13
RTTI
├── typeid → 정적/동적 RTTI 구분
├── dynamic_cast → 실패 시 nullptr vs bad_cast
│   └── static_cast 비교 → UB 위험
└── -fno-rtti → 성능 트레이드오프

RAII
├── unique_ptr → 단독 소유, move semantics
├── shared_ptr → 참조 카운팅, 순환 참조 문제
│   └── weak_ptr → 순환 참조 해결
├── lock_guard → 뮤텍스 + RAII
└── virtual 소멸자 (★꼬리질문 연결!)
    └── vtable → 동적 디스패치 (전날 주제 복습)

2. RTTI — Runtime Type Information

핵심 한 문장

RTTI는 C++이 런타임에 객체의 실제 타입 정보를 얻을 수 있게 하는 메커니즘으로, typeiddynamic_cast의 기반 인프라입니다.

동작 원리

virtual 함수가 있는 클래스는 vtable을 가지며, vtable 헤더에 type_info 포인터가 자동 삽입됩니다.

1
2
3
4
5
vtable 구조:
  [ type_info*     ]  ← RTTI 데이터 (vtable[-1])
  [ offset_to_top  ]  ← 다중 상속용 포인터 조정값 (vtable[-2])
  [ Func1*         ]  ← 가상 함수 [0]
  [ Func2*         ]  ← 가상 함수 [1]

typeid — 타입 이름 확인

1
2
3
4
5
6
7
8
9
10
11
12
#include <typeinfo>

class Animal { public: virtual ~Animal() {} };
class Dog : public Animal {};

Animal* a = new Dog();

// 정적 RTTI (컴파일 타임) — 포인터 타입 기준
std::cout << typeid(a).name();    // "Animal*"

// 동적 RTTI (런타임) — vptr → type_info 접근
std::cout << typeid(*a).name();   // "Dog"  ← 실제 타입!

dynamic_cast — 안전한 다운캐스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Animal* a = new Dog();

// 성공 케이스
Dog* d = dynamic_cast<Dog*>(a);    // Dog 맞음 → 유효 포인터 반환
if (d) { d->Bark(); }

// 실패 케이스
Cat* c = dynamic_cast<Cat*>(a);    // Dog이지 Cat이 아님 → nullptr 반환
if (!c) { /* 안전하게 처리 */ }

// 레퍼런스 버전 — 실패 시 std::bad_cast 예외 던짐
try {
    Cat& cr = dynamic_cast<Cat&>(*a);
} catch (const std::bad_cast& e) { }

dynamic_cast vs static_cast

 static_castdynamic_cast
검사 시점컴파일 타임런타임 (RTTI 사용)
실패 시UB (크래시 가능)nullptr / bad_cast
속도빠름느림 (vtable 탐색)
조건타입 확신할 때타입 불확실할 때 안전하게
RTTI 필요불필요필요
1
2
3
4
5
6
// static_cast 잘못된 사용 → UB
Animal* a = new Animal();
Dog* d = static_cast<Dog*>(a);  // 컴파일은 됨, 런타임 UB!

// dynamic_cast 안전한 사용
Dog* d = dynamic_cast<Dog*>(a);  // nullptr 반환 → 안전

RTTI 비용과 비활성화

  • 비용: type_info 구조체 크기 증가 + vtable 탐색 오버헤드
  • 비활성화: -fno-rtti (GCC/Clang) — 게임 엔진 등 성능 중심 환경
  • 비활성화 시 불가: dynamic_cast, typeid (컴파일 오류)

3. RAII — Resource Acquisition Is Initialization

핵심 한 문장

RAII는 자원의 수명을 객체의 수명에 묶는 C++ 관용구입니다. 생성자에서 자원을 획득하고, 소멸자에서 자원을 해제하여 예외가 발생해도 누수 없이 정리합니다.

문제: 예외 발생 시 자원 누수

1
2
3
4
5
6
// RAII 미적용 — 위험한 코드
void BadFunction() {
    int* data = new int[1000];
    DoSomething();   // 예외 발생 시 →
    delete[] data;   // 이 줄 실행 안 됨! → 메모리 누수
}

해결: RAII 패턴

1
2
3
4
5
6
// RAII 적용 — 스택 객체 소멸자가 자원 해제 보장
void GoodFunction() {
    std::unique_ptr<int[]> data = std::make_unique<int[]>(1000);
    DoSomething();   // 예외 발생해도 OK
    // 함수 종료 또는 예외 발생 → data 소멸자 자동 호출 → delete[] 실행
}

RAII 동작 원리

1
2
3
4
5
스택 프레임 종료 (정상 / 예외 / return) 시 스택 객체 소멸자 자동 호출
                    ↓
소멸자 안에서 자원 해제 (delete, fclose, unlock, ...)
                    ↓
자원 누수 없음 — 예외 안전(Exception Safety) 보장

표준 라이브러리 RAII 도구

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 메모리 관리
std::unique_ptr<T>  p = std::make_unique<T>();    // 단독 소유, 복사 불가
std::shared_ptr<T>  p = std::make_shared<T>();    // 참조 카운팅 공유 소유
std::weak_ptr<T>    w = shared;                   // 소유권 없는 약한 참조

// 2. 뮤텍스 잠금
std::mutex m;
{
    std::lock_guard<std::mutex> lock(m);  // 생성자: lock(), 소멸자: unlock()
    // 임계 구역
}  // 자동 unlock — 예외 발생해도 unlock 보장

// 3. 파일 핸들
std::ifstream file("data.txt");  // 생성자: fopen, 소멸자: fclose
// file이 스코프 종료 시 자동으로 닫힘

lock_guard — 뮤텍스 RAII 래퍼

멀티스레드 환경에서 공유 자원에 접근할 때 mutex로 잠금을 겁니다. 그런데 잠금 후 예외가 발생하면 unlock()이 호출되지 않아 데드락이 발생할 수 있습니다. lock_guard는 이걸 RAII로 해결합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::mutex m;
int shared_data = 0;

void Increment() {
    // RAII 미적용 — 위험
    m.lock();
    shared_data++;
    if (shared_data < 0) throw std::runtime_error("음수!");  // 예외 발생
    m.unlock();  // 예외 발생 시 이 줄 실행 안 됨 → 데드락!
}

void SafeIncrement() {
    std::lock_guard<std::mutex> lock(m);  // 생성자: m.lock()
    shared_data++;
    if (shared_data < 0) throw std::runtime_error("음수!");
    // 예외 발생해도 lock 소멸자에서 m.unlock() 자동 호출 → 데드락 없음
}
  • 생성자: mutex.lock() 호출
  • 소멸자: mutex.unlock() 호출
  • 스코프를 벗어나면 (정상 종료 / 예외 / return) 항상 unlock 보장

ifstream — 파일 핸들 RAII 래퍼

파일을 fopen으로 열면 반드시 fclose로 닫아야 합니다. 예외 발생 시 닫지 못하면 파일 디스크립터가 누수됩니다. ifstream은 이걸 RAII로 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C 스타일 — 위험
FILE* f = fopen("data.txt", "r");
ProcessData(f);   // 예외 발생 시
fclose(f);        // 이 줄 실행 안 됨 → 파일 핸들 누수

// ifstream RAII — 안전
{
    std::ifstream file("data.txt");  // 생성자: 파일 열기
    if (!file.is_open()) { /* 에러 처리 */ }

    std::string line;
    while (std::getline(file, line)) {
        ProcessLine(line);   // 예외 발생해도 OK
    }
}  // 소멸자: 파일 자동 닫힘 — fclose 직접 호출 불필요
  • 생성자: 파일 열기
  • 소멸자: 파일 자동 닫기
  • ofstream(쓰기), fstream(읽기+쓰기)도 동일한 RAII 패턴

RAII 직접 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
class ScopedArray {
    T* ptr;
public:
    explicit ScopedArray(size_t n) : ptr(new T[n]) { }
    ~ScopedArray() { delete[] ptr; }  // 소멸자에서 자원 해제

    // 복사 금지 (이중 해제 방지)
    ScopedArray(const ScopedArray&) = delete;
    ScopedArray& operator=(const ScopedArray&) = delete;

    T& operator[](size_t i) { return ptr[i]; }
};

void SafeFunction() {
    ScopedArray<int> data(1000);
    // 예외, return 무관 → data 소멸자가 delete[] 보장
}

RAII와 virtual 소멸자의 관계 (꼬리질문 연결!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// unique_ptr + 기반 클래스 포인터 사용 시
class Base {
public:
    ~Base() { }           // virtual 아님!
};

class Derived : public Base {
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() { delete[] data; }
};

std::unique_ptr<Base> p = std::make_unique<Derived>();
// p 소멸 → Base::~Base() 만 호출 → Derived::~Derived() 미호출 → 누수!

// 해결:
class Base {
public:
    virtual ~Base() { }   // virtual 소멸자 필수!
};
// 이제: p 소멸 → Derived::~Derived() → Base::~Base() 순서 정상 호출

이것이 RAII → 스마트 포인터 → virtual 소멸자 → vtable 꼬리질문 연결 경로입니다.


4. 꼬리질문 예상 경로

메인 질문 답변 후 예상 흐름

1
2
3
4
5
6
7
8
9
10
11
12
"RTTI와 RAII에 대해서 설명해 주세요"
         │
         ├─ RTTI 설명
         │    └─ "dynamic_cast와 static_cast 차이는?"
         │         └─ "RTTI 비용은?"  →  "-fno-rtti 옵션"
         │
         └─ RAII 설명
              └─ "스마트 포인터 종류와 차이는?"
                   ├─ unique_ptr vs shared_ptr vs weak_ptr
                   └─ "왜 unique_ptr<Base>에서 virtual 소멸자가 필요한가요?"
                        └─ "virtual 소멸자가 없으면 어떻게 되나요?"
                             └─ "그럼 vtable이 어떤 역할을 하나요?" ← 전날 주제로 회귀!

각 꼬리질문 30초 답변

Q: 스마트 포인터 3종 차이?

1
2
3
unique_ptr  — 단독 소유, 복사 불가, move만 가능. 오버헤드 제로.
shared_ptr  — 참조 카운팅 공유 소유. 카운트가 0이 되면 delete.
weak_ptr    — 소유권 없는 관찰자. 순환 참조 방지에 사용.

Q: shared_ptr 순환 참조란?

1
2
3
4
5
6
7
struct Node {
    std::shared_ptr<Node> next;  // 순환 참조 → 카운트 절대 0 안 됨 → 누수
};
// 해결: 한쪽을 weak_ptr로 교체
struct Node {
    std::weak_ptr<Node> next;   // 소유권 없음 → 순환 아님
};

Q: RAII와 예외 안전성의 관계?
스택 해제(stack unwinding) 시 소멸자가 반드시 호출되므로, 자원을 스택 객체(RAII 래퍼)로 관리하면 예외 발생 경로에서도 자원 해제가 보장됩니다.


5. 언리얼에서의 RTTI / RAII

언리얼 RTTI — Cast vs dynamic_cast

언리얼은 자체 리플렉션 시스템(UClass)을 사용하므로 dynamic_cast 대신 Cast<T>를 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
AActor* Actor = GetOwner();

// 언리얼 방식 (UObject 리플렉션 사용)
AMyCharacter* Char = Cast<AMyCharacter>(Actor);
if (Char) { /* 성공 */ }

// IsA — 타입 체크만
if (Actor->IsA<AMyCharacter>()) { }

// GetClass — type_info 대신 UClass 반환
UClass* cls = Actor->GetClass();
FString name = cls->GetName();   // "MyCharacter"

**Cast vs dynamic_cast**:

 dynamic_castCast<T> (언리얼)
기반C++ RTTI (vtable)UObject 리플렉션 (UClass)
UObject 아닌 타입사용 가능사용 불가
성능vtable 탐색UClass 체인 탐색
실패 시nullptrnullptr

언리얼 RAII — FScopeLock, TGuardValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// FScopeLock — std::lock_guard 언리얼 버전
FCriticalSection CriticalSection;
{
    FScopeLock Lock(&CriticalSection);  // 생성자: Lock(), 소멸자: Unlock()
    // 임계 구역
}  // 자동 Unlock

// TGuardValue — 임시 값 변경 후 자동 복원
bool bFlag = false;
{
    TGuardValue<bool> Guard(bFlag, true);  // bFlag = true
    DoSomething();
}  // bFlag = false 자동 복원

// FString, TArray — 소멸자에서 힙 메모리 자동 해제 (RAII 적용)
FString Str = TEXT("Hello");  // 내부적으로 TArray<TCHAR> 사용
// Str 소멸 → TArray 소멸자 → FMemory::Free 호출

참고

핵심 요약 — RTTI와 RAII는 결국 vtable에서 만난다. typeid/dynamic_cast는 vtable 헤더의 type_info*를 읽고, RAII를 다형 클래스에 쓰려면 virtual 소멸자(= vtable)가 필수라서, 어떤 꼬리질문이 와도 vtable로 답변을 이어갈 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.