【问题标题】:Design of Generic Singleton Wrapper class通用 Singleton Wrapper 类的设计
【发布时间】:2017-01-18 06:39:30
【问题描述】:

我们正在弃用我们项目中的 ACE 库,该库由大约 120 个二进制文件组成,并且在许多二进制文件中我们使用了 ACE_Singleton。因为在弃用后我们将不再有这个类,所以我们正在考虑编写自己的通用单例在所有这些二进制文件中通用的共享库中,我们想要实现的目标之一是如果有人从这个类继承(使用 CRTP)说 Logger,即使 Logger 构造函数是公共的,那么我们也不能创建两个 logger 对象。 为了说明,让我们假设我的单例类名称是 GenericSingleton,我的客户端类是记录器,那么下面的代码应该会抛出错误:

class Logger:public GenericSingleton<Logger>
{
   public:
      Logger()
      {
      }
};
int main()
{
   Logger obj;// first instance no issue
   Logger obj1; // second instance is problem as it is inherited from singleton 
}

那么有人可以建议我应该如何设计 GenericSingleton 以便在创建第二个对象时我应该得到编译时错误?

简而言之,如果我的派生类没有私有构造函数、析构函数复制构造函数等,那么可以在编译时使用 static_assert 进行检查

【问题讨论】:

  • 您也许可以使用例如std::once_flag 允许构造函数只被调用一次。但最常见的方法是根本没有公共构造函数,将其设为私有,并有一个公共static“getter”函数来获取该类的唯一实例。
  • 你说得对,单例类构造函数应该是私有的,但是在设计库函数时,我们不应该假设库用户会编写正确的客户端类,所以我想在库级别处理,如果某些类继承自 GenericSingleton 并且如果没有私有构造函数,那么应该会发生编译时错误
  • @Kapil 希望提供健壮且防错的库代码固然不错,但在某种程度上,您必须相信客户会为自己编写正确的代码。此外,还有许多错误(至少在 C++ 中)只是无法在编译时被捕获。
  • @Kyle 我认为完整的数据类型(例如 Logger)已传递给 GenericSingleton 模板类,所以我们不能检查该类型并确定它是否具有私有构造函数
  • @Kapil 如果您同意对子(单例)类实施实施某些限制,那么我想您会喜欢我添加到答案中的方法。

标签: c++ templates singleton


【解决方案1】:

构造函数无法在编译时知道它会在哪里或多少次被调用;构造函数只是函数,函数对它们的上下文一无所知。考虑到任何static_asserts 都将在编译类时进行评估,但这可能(而且几乎肯定会!)发生在与实际实例化类的代码完全不同的翻译单元中。

无论如何,这似乎不太可能有帮助,因为您必须有某种方法来访问整个代码库中的单例。

此外,不清楚为什么您希望允许您的单例具有公共构造函数。如果您只想通过添加继承声明来在编译时为完全任意的类强制执行单例行为,那么您就不走运了;可以任意构造任意类。

由于您是从 ACE 单例转换,我建议您使用类似的 API;请注意,ACE 单例文档建议将您的单例构造函数设为私有。

但是,如果您只想通过某种方式强制您的客户端编写一个不能(很容易)被不正确调用的构造函数,您可以执行以下操作:

template <typename T>
class SingletonBase {
  protected: class P { friend class SingletonBase<T>; P() = default; };
  public:
     SingletonBase(P) {}
     static T& Instance() {
         static T instance { P {} };
         return instance;
      }
};

(您还需要delete 基类的复制和移动构造函数。Here 是一个工作示例用例。请注意declaring P's constructor =default does not prevent the singleton class from default-initializing instances of P。)

现在,因为基类构造函数接受一个类型为 P 的参数,所以单例类实现必须将 P 传递给它的父类构造函数。但是由于 P 的构造函数是私有的,所以单例类将无法构造 P 的实例,除非通过复制或移动构造,因此它的构造函数必须采用 P 的实例。但是由于 P 本身是受保护的,所以只有单例类和父类实际上可以使用它,因此有效地对子构造函数的唯一可能调用必须在Instance 方法中。

请注意,您不需要显式声明和定义单例类的构造函数,因为需要使用SingletonBase&lt;Singleton&gt;::P,这会很丑陋。您可以简单地使用 using 声明公开构造函数:

using BASE = SingletonBase<Myclass>;
using BASE::SingletonBase;

【讨论】:

  • @Kapil 好吧,我是在没有 IDE 的智能手机上输入的;一些错别字等是可以预料的,老实说,这不是很有用的反馈。错误非常简单;我已经修复了它并添加了指向 Coliru 上一个工作示例的链接。
  • 对此表示歉意,并感谢您提供工作示例,我正在检查
  • 感谢您提出这种巧妙的方法,但这样我需要将 BASE::P 参数传递给每个客户端类,这看起来不太好
  • @Kapil 实际上,这似乎阻止单例类仅在默认构造函数中实例化新的 P。我不知道为什么,所以我会提出一个新问题。
  • 另外我认为我的 GenericSingleton getInstance 和构造函数是可变参数模板,因此如果某些客户端类在构造函数中需要参数,例如 Logger 类可能需要文件名,DBClass 可能需要数据库名、用户名、密码等。所以这也应该是可能的
【解决方案2】:

我的建议是将关注点分开。有服务(例如记录器)的概念,服务可能是也可能不是单例。但这是一个实现细节,因此是一个单独的问题。服务的消费者应该不知道它。

现在,在项目生命周期的后期,当您意识到单例是一个糟糕的想法时,您可以重构单例,而无需重构任何依赖于它的代码。

例如:

template<class Impl>
struct implements_singleton
{
    using impl_type = Impl;
    static impl_type& get_impl()
    {
        static impl_type _{};
        return _;
    }
};

struct logger_impl
{
    void log_line(std::string const& s)
    {
        std::clog << s << std::endl;
    }
};


struct logger
: private implements_singleton<logger_impl>
{
    void log_line(std::string const& s) {
        get_impl().log_line(s);
    }
};

void do_something(logger& l)
{
    l.log_line("c");
}

int main()
{
    logger a;
    logger b;

    a.log_line("a");
    b.log_line("b");   // behind the scenes this is the same logger
                       // but the user need not care

    do_something(a);    
}

【讨论】:

    【解决方案3】:

    您需要确保Logger 的实例不能在创建Logger 的唯一实例的函数之外创建。

    这是一个简单的实现。

    template <typename T> class GenericSingleton {
       public:
          static T& instance() {
             static T instance_;
             return instance_;
          }
    };
    
    class Logger: public GenericSingleton<Logger>
    {
       // Make sure that the base class can create the sole instance of
       // this class.
       friend GenericSingleton<Logger>;
    
       private:
    
          // Makes sure that you cannot create objects of the class
          // outside GenericSingleton<Logger>
          ~Logger() {}
    
       public:
          void foo() {}
    };
    
    int main()
    {
       // OK. Call foo() on the only instance of the class.
       Logger::instance().foo();
    
       // Not OK.
       Logger obj1;
    }
    

    【讨论】:

    • 这里你依赖于客户端类来使其析构函数私有,如果错误地没有在客户端类中完成,那么代码将正确编译?
    • @R Sahu 所以我的目标是 GenericSingleton 的设计方式是即使客户端类具有公共构造函数、析构函数、复制构造函数等,也应该只允许类的一个对象,如果有人尝试创建其他对象应该是编译错误
    • @Kapil,我认为您无法做到这一点。基类对派生类能做什么或不能做什么没有太多控制。基类唯一可以阻止派生类做的事情是通过将 virtual 函数声明为 final 来覆盖它。
    • @R Sahu,实际上我们可以抛出异常,因为在创建客户端类对象时将调用基类构造函数,因此我们可以检查通用单例构造函数,如果某个计数 > 1 然后抛出异常但我们想要在编译期间进行控制
    • @Kapil,就像我说的,我不认为你能做到。
    猜你喜欢
    • 1970-01-01
    • 2019-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-07
    • 1970-01-01
    • 2016-09-05
    相关资源
    最近更新 更多