【问题标题】:Implementing Singleton with CRTP使用 CRTP 实现单例
【发布时间】:2018-08-22 20:31:22
【问题描述】:

阅读this answer 后,我尝试实现一些简单的 CRTP 用法。我想我会尝试实现单例(是的,我知道 - 它只是为了练习和研究)模式,因为链接的答案有点已经做到了......除了事实上它不能编译。

引用代码如下:

template <class ActualClass> 
class Singleton
{
   public:
     static ActualClass& GetInstance()
     {
       if(p == nullptr)
         p = new ActualClass;
       return *p; 
     }

   protected:
     static ActualClass* p;
   private:
     Singleton(){}
     Singleton(Singleton const &);
     Singleton& operator = (Singleton const &); 
};
template <class T>
T* Singleton<T>::p = nullptr;

class A: public Singleton<A>
{
    //Rest of functionality for class A
};

然后我将其“现代化”为:

template <class T>
class Singleton {
public:
    Singleton()                              = delete;
    Singleton(const Singleton&)              = delete;
    Singleton(Singleton&&)                   = delete;
    Singleton& operator = (const Singleton&) = delete;
    Singleton& operator = (Singleton&&)      = delete;

    static T& get_instance() {
        if(!instance)
            instance = new T;
        return *instance;
    }

   protected:
     static inline T* instance = nullptr;
};

class A: public Singleton<A> {
    //Rest of functionality for class A
};

然后我尝试创建对实例的引用:

auto& x = A::get_instance();

显然没有编译。

值得一提的是,我收到了非常相似的错误消息,特别是:

注意:'A::A()' 被隐式删除,因为默认定义格式错误:class A : public Singleton&lt;A&gt;

显然,代码的第二个sn-p无法编译,因为我们删除了默认构造函数并尝试在get_instance 方法中将其与new T 一起使用。

令我惊讶的是,第一个 sn-p 也无法编译,并带有类似的错误消息。链接的答案有错误吗?我将如何使用 CRTP 为单例实现 通用基类/接口

【问题讨论】:

  • 您是否考虑过实现单例反模式并为自己省去很多麻烦?
  • @JesperJuhl,嗯,您是否考虑过阅读“是的,我知道 - 这只是为了实践和研究”部分?我无意在实际代码中使用它。此外,单例模式的用处不是这个问题的主题。我鼓励您阅读链接答案下方的第 6 条评论 - 获得 45 票的评论。
  • 我仍然认为这是一个错误的练习题选择。

标签: c++ singleton crtp


【解决方案1】:

这里是“现代化”sn-p 的模组:

template <class T>
class Singleton {
public:
    Singleton& operator = (const Singleton&) = delete;
    Singleton& operator = (Singleton&&)      = delete;

    static T& get_instance() {
        if(!instance)
            instance = new T_Instance;
        return *instance;
    }

protected:
    Singleton() {}

private:
    struct T_Instance : public T {
        T_Instance() : T() {}
    };

    static inline T* instance = nullptr;
};

class A : public Singleton<A> {
protected:
    A() {}
};

int main()
{
    auto& x = A::get_instance();
}

sn-p 的变化总结:

  • protected 单例中的默认构造函数
  • private 嵌套结构访问派生类的受保护构造函数
  • protected 派生类中的构造函数以防止实例化

另外,不需要 delete 通过向 Singleton 类添加默认 ctor 实现来隐式删除的构造函数。

不像Richard Hodges'示例那么小,但静态instance 成员可以轻松添加delete_instance() 方法以用于自动化单元测试。

【讨论】:

    【解决方案2】:

    您的第一个代码块的问题是Singleton(){} 被标记为私有。这意味着A 无法访问它,因此A 不能是默认构造。使构造函数protected 将解决该问题

    template <class ActualClass> 
    class Singleton
    {
       public:
         static ActualClass& GetInstance()
         {
           if(p == nullptr)
             p = new ActualClass;
           return *p; 
         }
    
       protected:
         static ActualClass* p;
         Singleton(){}
       private:
         Singleton(Singleton const &);
         Singleton& operator = (Singleton const &); 
    };
    template <class T>
    T* Singleton<T>::p = nullptr;
    
    class A: public Singleton<A>
    {
        //Rest of functionality for class A
    };
    
    int main()
    {
        auto& x = Singleton<A>::GetInstance();
    }
    

    您的第二个代码块有类似的问题,但默认构造不是private,而是您将其标记为delete,因此它不是=default 可构造意味着A 也不是默认可构造的。默认构造函数使其成为protected,就像第一个示例将解决该问题

    template <class T>
    class Singleton {
    public:
    
        Singleton(const Singleton&)              = delete;
        Singleton(Singleton&&)                   = delete;
        Singleton& operator = (const Singleton&) = delete;
        Singleton& operator = (Singleton&&)      = delete;
    
        static T& get_instance() {
            if(!instance)
                instance = new T;
            return *instance;
        }
    
    protected:
        Singleton()                              = default;
        static inline T* instance = nullptr;
    };
    
    class A: public Singleton<A> {
        //Rest of functionality for class A
    };
    
    int main()
    {
        auto& x = Singleton<A>::get_instance();
    }
    

    【讨论】:

    • 它还使得创建A 类型的对象成为可能,这正是我在实现单例时要避免的事情
    • @Fureeish 你只需要将A的构造函数也设为protected
    • 如果我这样做,您的代码将停止编译并出现'constexpr A::A()' is protected within this context 错误。
    • @Fureeish 我忘了。您还必须将friend class Singleton&lt;A&gt;; 添加到A
    • 那么这意味着最初链接的答案does有错误吗?它缺少派生类中的protected 构造函数和friend 声明。 // Rest of functionality for class A 是否暗示它们?尽管如此,谢谢你,你一直很有帮助
    【解决方案3】:

    尽可能小的(我认为)实现。

    特点:

    • A 既不可复制、不可构造也不可移动。 (通过删除复制操作隐式删除移动运算符)
    • 实现的构造是线程安全的。
    • 确保在程序结束时销毁实现。

     

    template <class T>
    struct Singleton 
    {
        Singleton(const Singleton&)              = delete;
        Singleton& operator = (const Singleton&) = delete;
    
        static T& get_instance() {
            static T _{allow()};
            return _;
        }
    
    private:
        struct allow {};
    
    protected:
        Singleton(allow) {}
    };
    
    class A: public Singleton<A> {
        using Singleton<A>::Singleton;
        //Rest of functionality for class A
    };
    
    int main()
    {
        auto& x = Singleton<A>::get_instance();
        auto& y = A::get_instance();
    
    // compiler error
        auto z = A();
    }
    

    但为什么不将“单一性”作为实现细节呢?为什么用户需要知道对象是单例?

    template <class T>
    struct Singleton 
    {
    protected:
        static T& get_impl() {
            static T _;
            return _;
        }
    };
    
    // the real implementation of A
    struct AImpl
    {
        void foo();
    };
    
    // A is a value-type which just happens to be implemented in terms of a
    // single instance
    struct A: public Singleton<AImpl> 
    {
        auto foo() { return get_impl().foo(); }
    };
    
    void bar(A a)
    {
        a.foo();
    }
    
    int main()
    {
        auto x = A();
        x.foo();
    
        auto y = A();
        y.foo();
    
        x = y;
    
        bar(x);
    }
    

    然后,如果您决定该类型不应该是单例,则无需更改其接口(因此您的程序的其余部分):

    示例 - A 是单例,B 不是。接口是相同的。

    #include <memory>
    
    template <class T>
    struct Singleton 
    {
    protected:
        static T& get_impl() {
            static T _;
            return _;
        }
    };
    
    template<class T>
    struct CopyableIndirect
    {
        CopyableIndirect() = default;
    
        CopyableIndirect(CopyableIndirect const& r)
        : impl_(std::make_unique<T>(*r.impl_))
        {
    
        }
    
        CopyableIndirect(CopyableIndirect&& r)
        : impl_(std::move(r.impl_))
        {
    
        }
    
        CopyableIndirect& operator=(CopyableIndirect const& r)
        {
            auto temp = r;
            swap(temp);
            return *this;
        }
    
        CopyableIndirect& operator=(CopyableIndirect && r)
        {
            auto temp = std::move(r);
            swap(temp);
            return *this;
        }
    
        void swap(CopyableIndirect& r)
        {
            std::swap(impl_, r.impl_);
        }
    protected:
        T& get_impl() {
            return *impl_;
        }
    
        T const& get_impl() const {
            return *impl_;
        }
    
       std::unique_ptr<T> impl_ = std::make_unique<T>();
    };
    
    struct AImpl
    {
        void foo() const;
    };
    
    struct A: public Singleton<AImpl> 
    {
        auto foo() const { return get_impl().foo(); }
    };
    
    struct B: public CopyableIndirect<AImpl> 
    {
        auto foo() const { return get_impl().foo(); }
    };
    
    void bar(A const& a)
    {
        a.foo();
    }
    
    void bar(B const& a)
    {
        a.foo();
    }
    
    int main()
    {
        auto x = A();
        x.foo();
    
        auto y = B();
        y.foo();
    
        bar(x);
        bar(y);
    }
    

    【讨论】:

    • 关于您的第一个 sn-p 代码,取消注释 auto z = A(); 不会导致编译错误。关于你的第二个 sn-p,我看不出那是一个单例。关于你的第三个 sn-p,几乎一样
    • @Fureeish 你是对的,默认的构造函数是由 A 继承的。sn-ps 2 和 3 的观点是,从实现的角度来看,实现的单例性质是一个无关紧要的细节该对象的所有用户,因为(无状态)对象的所有副本都是等价的并且引用相同的实现。
    • Visual Studio 2015 无法在 get_instance 的第 8 行使用 error C2248: 'A::A': cannot access protected member declared in class 'A' 编译第一个 sn-p。将 Singleton(allow) 访问修饰符更改为 public 可以修复错误,auto z = A(); 仍然无法按预期编译。在godbolt 中也进行了测试。
    • @Stranger1399 您提供的链接中的代码似乎与此答案无关?
    • @RichardHodges 我刚刚给出了网站主页的链接。我按原样复制粘贴了答案中的代码(评论auto z = A();)。这是带有代码的link。仅在 msvc 编译器上编译失败。
    猜你喜欢
    • 2021-10-30
    • 2017-11-11
    • 1970-01-01
    • 1970-01-01
    • 2021-10-21
    • 1970-01-01
    • 1970-01-01
    • 2023-01-12
    • 2023-03-18
    相关资源
    最近更新 更多