본문 바로가기

프로그래밍 언어/C++

프로그램 종료 과정 이해하기

반응형

non-main 쓰레드가 초기 호출 함수에서 벗어나면

새로운 쓰레드가 초기 호출 함수에서 벗어나면 std::exit 함수가 역시 호출되고, std::exit 함수 내부에서 쓰레드 실행 과정에서 정상적인 과정을 거쳐 생성된 thread storage duration 객체의 소멸자가 호출된다.

main 쓰레드가 main 함수에서 벗어나면

main 함수를 벗어나면, 가장 먼저 main 함수의 반환값을 argument로 std::exit 함수를 호출한다. std::exit 함수 내부에서 정상적인 과정을 거쳐 생성된 static storage duration 객체 생성 시점 또는 std::atexit 함수로 등록한 함수를 전체적으로 시간 순서로 배열한 역순으로 소멸자나 등록 함수를 번걸아 호출 완료한다.

thread stroage duration 객체과 static storage duration 객체에 대한 생성자/소멸자

static storage duration 객체의 초기화는 크게 static initializationdynamic initialization으로 구분된다. 두 가지 유형 중 한 방식으로 먼저 초기화가 완료된 객체는 역순으로 소멸자 호출를 완료한다.

모든 thread storage duration 객체는 static storage duration 객체보다 소멸자 호출를 먼저 완료한다.

배열 type이나 class type 객체의 subobject 생성 과정에서 생성한 static storage duration의 block 변수를 생성하더라도, static storage duration의 block 변수의 생성보다 subobject의 소멸자 호출을 먼저 완료한다.

#include <print>

class A {
    struct nested_A {
        int val;
        nested_A(int v) :val{ v } {};
        ~nested_A() {
            std::println("nested_A destructor: {}", val);
        }
    };

    nested_A   nested_;
public:
    A(int v) :nested_{ v } {
        static nested_A local_a{ v * 100 };
    };
};

int main() {
    A b { 1 };
    return 0;
}
[출력 결과]
nested_A destructor: 1
nested_A destructor: 100

static duration 또는 thread duration 객체의 소멸자 호출 과정에서 exception으로 인해 소멸자 호출을 완료하지 않고 벗어나면, std::terminate 함수가 호출된다.

static 또는 thread storage duration의 소멸자에서 함수를 호출하고, control flow가 이미 소멸된 static 또는 thread storage duration의 block 변수를 통과하거나, 간접적으로(예를 들어 포인터를 통해) 사용하면, undefined behavior다.

static storage duration 객체의 초기화가 std::atexit 함수 호출보다 먼저 완료하면, std::atexit로 등록한 함수 호출을 객체 소멸자 호출보다 먼저 완료한다. 반대로 std::atexit 함수 호출을 static storage duration 객체의 초기화보다 먼저 호출하면, 객체 소멸자 호출을 std::atexit로 등록한 함수 호출보다 먼저 완료한다.

std::atexit 함수로 등록한 함수를 등록 역순으로 호출한다.

#include <print>

class A {
    int val_{};
public:
    A(int i) :val_{ i } {}
    ~A() { std::println("~A:{}", val_); }
};

void reg1() {
    std::println("reg1");
}

void reg2() {
    std::println("reg2");
}

class B {
public:
    B() {
        std::atexit(reg1);
        std::atexit(reg2);
    }
};

A a{ 1 };
B b{};
A a100{ 100 };

int main() {
    return 0;
}

[출력 결과]
~A:100
reg2
reg1
~A:1

static storage duration 객체의 소멸자과 std::atexit로 등록한 함수 호출 과정에서 사용할 수 있다고 명시하지 않는 표준 라이브러리의 객체나 함수를 signal handler에서 사용하면, undefined behavior다. static storage duration 객체의 소멸자 호출이 완료된 이후에, 해당 객체를 사용하면 undefined behavior다. std::exit 호출하기 이전 또는 main 함수에서 벗어나기 이전에 모든 쓰레드를 종료한 경우, static storage duration의 소멸자가 호출되지 않은 상태에서는 이들 객체에 대한 접근은 undefined behavior가 아니다.

std::abort 함수

std::abort 함수 호출은 std::atexitstd::at_quick_exit 함수로 등록한 함수나, 생성한 객체의 소멸자 호출을 수행하지 않고, 프로그램을 곧바로 종료한다.

std::terminate 함수

std::terminate 함수는 C++ runtime에서 지원하는 함수다. 즉, C++에서 정의한 함수다. exception 제어 과정에서 구현상의 미묘한 문제로 인해, 더 이상 exception를 제어할 수 없는 상태가 되면, std::terminate 함수를 호출하도록 설계했다. std::terminate 함수는 C++의 exception 제어과 연관된 함수다.

exception 제어의 미묘한 문제로 발생한 std::terminate 함수 호출 이전에 stack unwound 수행할지 여부는 구현 컴파일러마다 다르다. noexcept 함수가 exception하면 std::terminate를 호출되는데, std::terminate 함수 호출 이전에, stack unwound를 온전히 수행할지, 부분적으로 수행할지, 전혀 수행하지 않을지 여부는 구현 컴파일러가 선택한다.

지금까지 언급하지 않는 경우로 인한 std::terminate 함수 호출 이전에, stack unwound를 수행하지 않아야 한다. 구현 컴파일러는 std::termination 함수 호출이 최종적으로 프로그램을 종료한다는 이유로 stack unwound를 수행하도록 과도하게 구현하지 않아야 한다.

반응형