CRTP로 Singleton 만들기

안녕하세요 뉴비 개발자입니다.

여러 곳에서 Singleton 패턴으로 인스턴스를 생성하는 코드를 줄이기 위해 템플릿을 도입하는 과정을 정리해보았습니다.

처음에는 link를 참고하여 Meyers Singleton을 구현하였고 잘 동작하는 것을 확인했습니다.

class Singleton
{
public:
    static Singleton& GetInstance()
    {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

이 클래스를 템플릿으로 만들고 인스턴스에 접근했는데 아래처럼 에러가 발생하더군요

template <typename T>
class Singleton
{
public:
    static T& GetInstance()
    {
        static T instance;
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

class A : public Singleton<A> {...};

int main()
{
    ....
    A::GetInstance();
    ...
}
warning C4624:  'A': destructor was implicitly defined as deleted
error C2280:  'A::A(void)': attempting to reference a deleted function

에러가 어디에서 발생하는 지는 알겠지만 제 실력으로는 해결하기 어려웠습니다.
왜 템플릿으로 바꿨더니 컴파일러가 소멸자를 삭제해버리는 걸까요?

구글링해도 원하는 정보가 없던 차에 우연히 boost에서 Singleton을 어떻게 만드는지 참고해서 작성해봤습니다.

class NonCopyable
{
protected:
    constexpr NonCopyable() = default;
    ~NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

template <typename T>
class Singleton : private NonCopyable
{
public:
    static T& GetInstance()
    {
        static T instance;
        return instance;
    }
};

NonCopyable 클래스로 복사가 안되게 하는 정책을 만들어서 상속시켰더니 잘만 실행됩니다.

이제 boost에서 왜 생성자에만 constexpr을 달아놨는지 찾아봐야겠습니다…

저도 궁금하네요 함수에 constexpr을 붙이는 이유는 컴파일 타임에 평가되기를 요청하는 것인데, NonCopyable 생성자에 저것을 붙일까용

요거 한 번 곰곰이 생각을 해봤는데, 역시 아무리 생각해도 컴파일 타임에 평가되는 거 빼곤 잘 모르겠더라고요. 그래서 검색을 해봤는데,

전 추상적으로 접근해서 컴파일 타임 평가를 코드화 시켜서 생각을 안했는데,
두 번째 문답에서 제시해준 예제가 적절한 것 같읍니다.

또, 이런 내용도 있는데

https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.2.0/com.ibm.zos.v2r2.cbclx01/constexpr_constructors.htm

상단부에

A constructor that is declared with a constexpr specifier is a constexpr constructor. 
Previously, only expressions of built-in types could be valid constant expressions. 
With constexpr constructors, objects of user-defined types can be included in valid constant expressions.

이것도 좀 깔끔하게 정리된 것 같읍니다

그야 private 로 설정한 template의 생성 / 소멸자를 A 에서 상속해 접근하니까요.
접근 되면 컴파일러가 맛이간거죠. ( 당연히 protected 로 하면 컴파일되죠. )

제가 상속관계를 잘못 이해고 있었네요;;


이거보고 해결했습니다

머리를 정리할 겸 코드도 정리해봤습니다.

#include <cxxabi.h>
#include <iostream>
#include <typeinfo>

using namespace std;

template <typename T>
class Singleton
{
public:
    static T &GetInstance()
    {
        static T instance;
        return instance;
    }
    void print()
    {
        // this prints like 1A, 1B
        // cout << typeid(T).name() << endl; 
        
        cout << abi::__cxa_demangle(typeid(T).name(), 0, 0, 0) 
             << endl;
    }
protected:
    Singleton() = default;
    ~Singleton() = default;
private:
    Singleton(const Singleton &) = delete;
    Singleton &operator=(const Singleton &) = delete;
};

class A : public Singleton<A> {};
class B : public Singleton<B> {};

int main()
{
    A::GetInstance().print();
    B::GetInstance().print();
}
output:
A
B

클래스 이름을 출력하려고 typeid(T).name()을 써봤는데 제대로 안나와서 abi::__cxa_demangle을 이용했습니다. (https://stackoverflow.com/a/3649351 참고)

Compiler Explorer 에서 직접 실행해보세요~

2 Likes

정적 다형성을 고려한 예제도 만들어봤습니다.

singleton.h

// Singleton with CRTP
// Running in Browser : https://godbolt.org/z/J-ZcA8
// References : https://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton
//              https://www.modernescpp.com/index.php/component/content/article/42-blog/functional/273-c-is-still-lazy

#pragma once

#include <iostream>

template <typename T>
class Singleton {
public:
    static T& GetInstance()
    {
        static T instance;
        return instance;
    }

    void Interface()
    {
        static_cast<T*>(this)->Implementation();
    }

    void Implementation()
    {
        std::cout << "Implementation Base" << std::endl;
    }

protected:
    Singleton() = default;
    ~Singleton() = default;

private:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

singleton.cpp

#include "singleton.h"
#include <iostream>

class A : public Singleton<A> {
public:
    void Implementation()
    {
        std::cout << "Implementation A" << std::endl;
    }
};

class B : public Singleton<B> {
public:
    void Implementation()
    {
        std::cout << "Implementation B" << std::endl;
    }
};

class C : public Singleton<C> {
};

template <typename T>
void Execute(T& base)
{
    base.Interface();
}

int main()
{
    Execute(A::GetInstance());
    Execute(B::GetInstance());
    Execute(C::GetInstance());

    return 0;
}

output

Implementation A
Implementation B
Implementation Base