C++ Singleton 类——继承的好习惯

Posted

技术标签:

【中文标题】C++ Singleton 类——继承的好习惯【英文标题】:C++ Singleton class - inheritance good practice 【发布时间】:2012-03-11 08:40:28 【问题描述】:

在现有项目中,我将继承一个声明为 Singleton 的 Controller 类(MVC),以便定义我自己的处理方式。如何适当地派生这个 Singleton 类?

首先,我扩展上下文和对这种继承的需求。

我添加到现有软件中的应用程序想要使用一个 MVC 模块,该模块执行与我愿意执行的任务几乎相同的任务。它使用相同的方法进行签名和轻微修改。重写我自己的 MVC 模块肯定是重复代码。现有模块本质上是面向软件另一部分的应用,我不能简单地使用相同的模块。但是被写成一个模型-视图-控制器模式,其中控制器是单例。我已经派生了 View。

其次,我怀疑我能否经典地派生 Singleton 类。

从继承类调用构造函数只会为父类调用 getinstance() 并且无法从派生类返回对象 (?)。

第三,这是我看到的一些处理方式。请评论/帮助我改进!

我将整个 Singleton 类复制到一个可以称为 AbstractController 的类中。我派生了两次这个类。第一个孩子是单例,采用父类的整体处理。第二个孩子是我的应用程序部分的控制器,具有自己重新定义的处理方式。

谢谢!

【问题讨论】:

在大多数情况下,如果一个类的基类是单例的,那么它完全没用:你永远不能创建派生类的任何实例,因为每个这样的实例也是基类,你不能再做一个。 (我可以想象“合理”的例外情况,但仅限于基类的设计专门考虑到派生的想法......) 【参考方案1】:

我不确定我是否完全理解您正在处理的情况,从单例派生是否可能或合适在很大程度上取决于单例的实现方式。

但由于您提到“良好做法”,因此在阅读问题时会想到一些一般要点:

    继承通常不是实现代码重用的最佳工具。见:Prefer composition over inheritance?

    使用单例和“良好实践”通常不能同时使用!见:What is so bad about singletons?

希望对您有所帮助。

【讨论】:

【参考方案2】:

事实是,单例和继承不能很好地结合在一起。

是的,是的,Singleton 爱好者和 GoF 崇拜者会因此而对我说:“好吧,如果你让你的构造函数受到保护......”和“你没有必须在课堂上有一个getInstance 方法,你可以把它...”,但他们只是证明了我的观点。单例必须经过许多圈才能成为单例和基类。

但只是为了回答这个问题,假设我们有一个单例基类。它甚至可以在某种程度上通过继承来加强它的单一性。 (当构造函数不再是私有的时,它会做少数可以工作的事情之一:如果另一个Base 已经存在,它会抛出异常。)假设我们还有一个继承自Base 的类Derived。由于我们允许继承,假设Base 的其他子类可以有任意数量,它们可能会或可能不会从Derived 继承。

但是有一个问题 - 您已经遇到或即将遇到的问题。如果我们在没有构造对象的情况下调用Base::getInstance,我们将得到一个空指针。我们想取回任何存在的单例对象(它可能是Base,和/或Derived,和/或Other)。但要做到这一点并仍然遵守所有规则是很困难的,因为只有几种方法可以做到——而且它们都有一些缺点。

我们可以创建一个Base 并返回它。拧紧DerivedOther。最终结果:Base::getInstance() 始终准确返回 Base。儿童班永远不会玩。有点违背了目的,IMO。

我们可以在派生类中放置一个我们自己的getInstance,如果调用者特别想要一个Derived,可以让他们说Derived::getInstance()。这显着增加了耦合度(因为调用者现在必须知道具体请求 Derived,并最终将自己绑定到该实现)。

我们可以做最后一个的变体——但不是获取实例,函数只是创建一个。 (当我们这样做时,让我们将函数重命名为initInstance,因为我们并不特别关心它得到了什么——我们只是调用它以便它创建一个新的Derived 并将其设置为 One真实实例。)

所以(除非有任何未解释的奇怪情况),结果有点像这样......

class Base 
    static Base * theOneTrueInstance;

  public:
    static Base & getInstance() 
        if (!theOneTrueInstance) initInstance();
        return *theOneTrueInstance;
    
    static void initInstance()  new Base; 

  protected:
    Base() 
         if (theOneTrueInstance) throw std::logic_error("Instance already exists");
         theOneTrueInstance = this;
    

    virtual ~Base()   // so random strangers can't delete me
;

Base* Base::theOneTrueInstance = 0;


class Derived : public Base 
  public:
    static void initInstance() 
        new Derived;  // Derived() calls Base(), which sets this as "the instance"
    

  protected:
    Derived()     // so we can't be instantiated by outsiders
    ~Derived()    // so random strangers can't delete me
;

在你的初始化代码中,你说Base::initInstance();Derived::initInstance();,这取决于你希望单例是哪种类型。当然,您必须从Base::getInstance() 转换返回值才能使用任何Derived 特定的函数,但无需转换,您可以使用Base 定义的任何函数,包括被Derived 覆盖的虚函数.

请注意,这种方式也有其自身的一些缺点:

它将强制单一性的大部分负担放在基类上。如果底座没有这个或类似的功能,而你又不能改变它,那你就完蛋了。

不过,基类不能承担所有的责任——每个类都需要声明一个受保护的析构函数,或者有人可能会在强制转换后删除一个实例(不)适当地,整个事情都下地狱了。更糟糕的是,编译器无法强制执行此操作。

因为我们使用受保护的析构函数来防止一些随机的 schmuck 删除我们的实例,除非编译器比我担心的更聪明,否则即使运行时也无法在程序结束时正确删除您的实例.再见,RAII ...你好“检测到内存泄漏”警告。 (当然,内存最终会被任何体面的操作系统回收。但如果析构函数没有运行,你就不能依赖它为你做清理。你需要在你之前调用某种清理函数退出,这不会给你任何接近 RAII 可以给你的保证。)

它公开了一个initInstance 方法,IMO 并不真正属于每个人都可以看到的 API。如果您愿意,您可以将 initInstance 设为私有,并让您的 init 函数成为 friend,但随后您的类对自身外部的代码进行了假设,而耦合又重新发挥作用。

还要注意,上面的代码根本不是线程安全的。如果你需要,你就靠自己了。

说真的,不那么痛苦的方法是忘记强迫单身。确保只有一个实例的最简单的方法是只创建一个。如果您需要在多个地方使用它,请考虑依赖注入。 (非框架版本相当于“将对象传递给需要它的东西”。:P)我去设计上面的东西只是为了试图证明自己在单例和继承方面是错误的,只是向自己重申了多么邪恶组合是。我不建议在实际代码中实际使用它。

【讨论】:

这是一个很好的答案,而且非常详细,并且有一些丰富多彩的语言和幽默。它在 SO 雷达下飞行真是太神奇了。 “它将大部分强制单一性的负担放在基类上。” - 在某些情况下,这可能正是我们想要的。比如说,我们将基类实例作为 Api 暴露给外部世界,并隐藏派生类,它只能被内部代码引用。 @ShibirBasak:问题是,它永远不是你想要的。它导致的问题比它所能解决的要多。例如,它只是“大多数”,因为它不可能是“全部”。双方都必须这样做,否则您将失去 Singleton 模式旨在提供的所有保证。一旦你准备好放弃这些,你就已经放弃了首先遵循这种模式的理由。【参考方案3】:

我最近在我的应用程序中也有类似的需求......无论如何,这是我的代码外实现:

h.

    class icProjectManagerHandler;
    class icProjectManager : public bs::icBaseManager 
        friend class icProjectManagerHandler;
    protected:
        icProjectManager();
    public:
        ~icProjectManager();

        template<typename t>
        static t *PM() 
            return dynamic_cast<t *>(icProjectManagerHandler::PMH()->mCurrentManager);
        ;
        template<typename t>
        static t *PMS() 
            static icProjectManagerHandler pm;
            return static_cast<t *>(icProjectManagerHandler::PMH()->mCurrentManager);
        ;
    ;

    class icProjectManagerHandler 
        friend class icProjectManager;
        icProjectManager *mCurrentManager;
        icProjectManagerHandler();
    public:
        ~icProjectManagerHandler();
        static icProjectManagerHandler *PMH();

        inline void setProjectManager(icProjectManager *pm) 
            if (mCurrentManager)  delete mCurrentManager; 
            mCurrentManager = pm;
        
    ;

Cpp.

    icProjectManagerHandler::icProjectManagerHandler() 
        mCurrentManager = new icProjectManager();
    
    icProjectManagerHandler::~icProjectManagerHandler() 

    
    icProjectManagerHandler *icProjectManagerHandler::PMH() 
        static icProjectManagerHandler pmh;
        return &pmh;
    
    icProjectManager::icProjectManager() 

    
    icProjectManager::~icProjectManager() 

    

还有例子:

class icProjectX : public ic::project::icProjectManager 
public:
    icProjectX() ;
    ~icProjectX() ;
;

int main(int argc, char *argv[]) 
    auto pro = new icProjectX();
    pro->setIcName("Hello");
    ic::project::icProjectManagerHandler::PMH()->setProjectManager(pro);
    qDebug() << "\n" << pro << "\n" << ic::project::icProjectManager::PMS<icProjectX>();
    return 10;

这个实现的问题是你必须首先初始化你的“单例”类,否则你会得到默认的基类。但除此之外......它应该可以工作吗?

【讨论】:

以上是关于C++ Singleton 类——继承的好习惯的主要内容,如果未能解决你的问题,请参考以下文章

C ++继承封闭类-这是一种好习惯吗?

在 C++ 中声明实例变量而不构造它们的好方法是啥? [关闭]

C++设计模式Singleton的侵入式和声明式

Java Reactor 中嵌套 flatMaps 的好习惯是啥?

记录一下应该养成的好习惯

c++友元模板单例模式