#cpp #idioms Nifty Counter

1 minute read

목적은 Construct On First Use와 같다. 즉, global static object, global object 초기화 순서가 명확하지 않아서 발생하는 static initialization order fiasco를 막기 위한 idiom이다.

Construct On First Use는 local static object가 초기화되는 시점을 이용했던 반면, Nifty Counter는 다른 translation unit이면 링커가 정렬해주는 순서에 군말 없이 따라야 하지만 같은 translation unit이면 소스 코드 상에 먼저 나타난 놈이 먼저 초기화된다는 사실을 이용한다. 그리고 Construct On First Use와 달리 다른 global static object, global object를 초기화해주는 보조적인 역할을 한다.

예제 코드를 살펴보자. 예제는 The Journeyman’s Shop: Initialization and Cleanup - Dr. Dobb’s에서 가져왔다.

// datalog.h

#include <iosfwd>
extern std::ofstream out;

class out_initializer
{
public:
    out_initializer() {
        if (init_count++ == 0)
            init_out();
    }
    ~out_initializer() {
        if (--init_count == 0)
            cleanup_out();
    }
private:
    static int init_count;
    static void init_out();
    static void cleanup_out();
};
static out_initializer out_initializer_object;

4번째 줄을 보면 out을 external linkage로 선언했다. 즉, datalog.cpp에 선언된 out을 다른 cpp에서도 사용한다는 뜻이다. 자 그럼 딱 떠오르는 걱정. 과연 out을 다른 cpp에 있는 global object에서 사용하려고 할 때, 초기화가 된 이후일까? 링커가 잘 알아서 초기화 순서를 정렬해주는 걸 찰떡같이 믿고 있으면 될까?

22번째 줄에서 해답이 나온다. 짜잔~ 헤더에 global static object를 선언했다. 헤더에 선언한 덕에 out을 쓰려고 datalog.h를 include한 모든 cpp에서 선언한 다른 global object보다 out_initializer_object가 먼저 나타난다. 그럼 같은 translation unit에서는 먼저 나타나는 놈이 먼저 초기화되기 때문에 안심하고 out은 초기화되어 있구나 하면서 사용할 수 있게 된다.

// datalog.cpp

#include "datalog.h"
std::ofstream out;

static int out_initializer::init_count;

void out_initializer::init_out() {
    new (&out) std::ofstream("test.dat");
}
void out_initializer::cleanup_out() {
    out.close();
}

레퍼런스 카운터로 사용할 static member를 초기화해주고 있다. 자 또 static이 나왔다. 이건 도대체 언제 초기화될까? 이런 명시적으로 초기화 해주는 함수가 없는 타입이면 런타임 초기화 구문이 돌기 전에 0으로 초기화된다. 그래서 걱정 없이 레퍼런스 카운터로 사용할 수 있으니 안심해도 된다.

문제점은 out을 사용하고 있는 모든 translation unit에 초기화를 보장해 줄 out_initializer_object가 생성된다는 거다. 물론 이 녀석은 static이기 때문에 프로그램이 종료될 때 소멸한다. 이런 오버헤드를 감수해야 한다.

참고