[C++] object는 언제 생성돼서 언제 소멸되는가 - storage

C++에서는 타입을 가지는 일정 크기의 값을 object라고 한다. object의 중요한 특징 중 하나는 lifetime. 즉, object가 언제 소멸하는가 하는 것이다. 이 object는 객체 지향 프로그래밍에서 말하는 객체와는 살짝 다른 개념이다. C++ 표준에서 object는 타입, alignment, lifetime 등의 특성을 가지는 일정 크기의 연속된 값을 의미한다. 이는 객체 지향 프로그래밍에서 말하는 객체와는 약간 다른 의미이지만 이 글에서는 그냥 객체라고 부르도록 하겠다. C++에서 객체의 lifetime을 알기 위해서는 우선 객체가 어디에 생성되는지를 알아야 한다. 일반적으로 C++ 개발자에게 객체가 어디에 생성되는지를 물으면 다음과 같이 대답할 것이다.

로컬 객체는 stack에, new로 생성된 객체는 heap에 저장되는데 stack과 heap은 같은 메모리 영역을 공유하며 stack은 메모리가 감소하는 방향으로 커지고, heap은 메모리가 증가하는 방향으로 커진다. 또한, 전역 변수 중 initialize 되지 않은 것은 bss에 initialize 된 것은 data 섹션에 저장된다.

이 대답은 기술적으로 맞는 대답이다. 사실 매우 훌륭한 대답이다. 실질적으로 대부분의 머신에서 대부분의 컴파일러는 위와 같은 방식으로 코드를 컴파일한다. 하지만 다시 한번 생각해보자. stack과 heap이 같은 메모리를 나누어 쓰는 이유는 한정된 크기의 메모리에서 컴파일 타임에 알 수 없는 두 종류의 동적 메모리를 할당하기 때문이고, bss 영역과 data 영역이 나누어지는 이유는 바이너리 파일의 크기를 줄이기 위한 최적화 때문이다. 즉, stack, heap, data, bss 등의 메모리 영역 등은 구현에 관련된 것일 뿐이다. 그렇다면 C++ 표준 문서에서는 이에 대해서 어떻게 기술하고 있을까?

C++ 표준에서는 객체가 메모리에 생성된다고 하지 않고, 스토리지에 생성된다고 한다. 구현과 표준을 분리하기 위한 결정으로 구체적인 메모리 레이아웃 등을 정하지 않음으로써 컴파일러들에게 실행 머신에 따라 최적화시킬 여지를 주기 위해서다. C++에는 원래 automatic, dynamic, static 3개의 스토리지가 있었고 C++11에서 thread_local이 추가되어 네 종류의 스토리지가 있다.

automatic storage

가장 기본적인(?) 스토리지는 automatic storage다. C++에 익숙한 사람이라면 스택 영역이라고 생각하면 된다. static specifier가 없는 로컬 객체는 automatic storage에 할당되는데, 이는 선언된 위치에서 생성되고, block을 떠날 때 생성된 역순으로 소멸된다. C++ 11 이전에 있었던 auto specifier는 객체를 automatic storage에 저장하라는 의미였다. 로컬 객체는 따른 specifier가 없으면 모두 automatic storage에 저장되므로 아무 의미가 없으므로 별로 사용되지 않았고 지금은 auto 키워드는 타입 추론과 관련된 의미로 변경됐다.

dynamic storage

dynamic storage는 다른 이름으로 free storage라고 부르기도 한다. new 키워드로 생성된 객체는 dynamic storage에 생성되고 명시적으로 delete가 불리기 전에는 소멸하지 않는다. 일반적으로 dynamic storage에 저장되는 object는 메모리의 힙 영역에 저장되도록 구현된다. delete가 불리지 않으면 소멸하지 않고, 같은 객체에 대해서 두 번 호출되면 메모리가 오염되는 문제가 생긴다. 따라서 dynamic storage에 생성되는 객체를 automatic storage에 생성되는 객체와 같은 라이프타임을 가지게 하는 RAII같은 기술이 많이 사용된다. 특히 C++11 이후의 modern C++에서는 shared_ptr이나 unique_ptr에 종속되지 않은 dynamic 객체가 존재하는 것을 좋지 않은 코드로 본다.

static storage

구현에서는 초기화됐는지 아닌지에 따라 data 영역과 bss 영역을 구분한다. 하지만 이는 바이너리 파일의 크기를 줄이기 위한 최적화일 뿐이고 표준 문서에서는 둘을 구분하지 않는다. 명시적으로 static 한정자가 붙었거나, namespace 스코프의 변수는 모두 static storage에 생성된다. 초기화됐건 선언만 했건 둘 다 static storage에 할당되고 그저 초기화되지 않은 static 객체는 0으로 초기화된다고 할 뿐이다.

static storage에 생성된 객체는 프로그램이 종료될 때, 생성이 완료된 순서의 역순으로 소멸한다. 하지만 프로그램이 어떻게 종료되더라도 언제나 소멸되는 것은 아니다. main 함수가 종료돼서 끝나거나 exit 함수에 의해서 종료되면 static 객체는 소멸하지만 termiate에 의해서 종료되거나 예외를 잡지 못해서 종료되면 static 객체는 소멸하지 않는다.

thread-local storage

thread-local storage는 C++11이 스레드를 지원하면서 새로 생긴 스토리지다. 변수 선언 시 thread_local 한정자를 붙이면 thread-local storage에 객체가 할당된다. 이는 static storage와 비슷하지만 스레드별로 생성되고 그 라이프타임도 스레드에 종속된다

보통의 컴파일러들이 thread local 객체를 구현하는 방법도 static 객체와 비슷하다. 선언만 된 객체들의 크기의 합과 초기화된 객체의 값이 따로 저장된다. 각 영역은 흔히 tbss와 tdata라고 불린다. 파일 포맷에서는 static object와 비슷하지만, 실행시킬 때는 사실 stack에 저장되기 때문에 메모리상에서는 스택과 비슷한 위치에 생성된다. 조금 더 정확히는 스레드별로 별개의 스택 시작 위치를 가지는데, 이때 스택의 시작 부분에 thread local 객체들을 생성한다.

thread local 객체는 스레드 시작에서 생성자가 불리고, 스레드가 종료될 때 소멸하는데 static 객체와 마찬가지로 예외에 의해서 종료되거나 terminate에 의해서 종료되면 thread local 객체는 소멸하지 않는다.

이상이 C++11이 가지고 있는 네 종류의 스토리지에 대한 설명이었다. 이 스트로지는 객체의 lifetime을 결정하기 때문에 잘 알고 있어야 한다. automatic storage에 할당된 객체는 그 레퍼런스가 해당 block을 벗어나지 않도록 해야 하고, dynamic storage에 할당된 객체는 반드시 한 번만 소멸하도록 신경 써서 코드를 작성해야 하며, static storage는 프로그램 전체에서 하나만 생기고, thread-local sotrage는 스레드마다 하나씩 생기며 스레드와 라이프타임을 공유한다. static storage와 thread-local storage에 생성된 객체의 소멸자는 생성자가 완료된 순서의 역순으로 호출된다는 것을 조심해야 한다. 소멸한 객체에 대한 접근은 잡기 힘든 버그를 만드니 자신이 만든 객체가 어떤 storage에 저장되고 어떤 lifetime을 가지는지 반드시 신경 써야 한다.

댓글

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

RAII는 무엇인가

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API