嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > Qt单例模式的玩法

Qt单例模式的玩法


一、导读

在实际Qt开发中,很多时候会使用到单例模式,例如:一个数据解析器、一个翻译文件管理器、一个样式表加载器等等,这些都可以抽象成单例,Qt中,提供了一个Q_GLOBAL_STATIC,用于具体的单例实现,本文来描述这个宏定义,并从源码角度看看其背后的实现。

二、Q_GLOBAL_STATIC简介

Q_GLOBAL_STATIC宏定义在5.1版本后引入。

定义如下:

Q_GLOBAL_STATIC(Type, VariableName, ...)

该宏用于创建一个全局静态对象,类型为QGlobalStatic,名称为VariableName,行为为指向Type的指针(详细过程会在后文中写到)。由Q_GLOBAL_STATIC创建的对象在第一次使用时初始化,因此不会增加应用程序或库的加载时间。此外,对象在所有平台上都是以线程安全的方式初始化。

这个宏的典型用法如下,在全局上下文中(也就是说,在任何函数体之外),使用类似下列的语句:

Q_GLOBAL_STATIC(MyType, staticType)

staticType是一个指向MyType的指针,则可以使用这个指针访问这个MyType的对象。

此宏旨在替换不是POD(Plain Old Data,C++11引入的一个概念)的全局静态对象。例如,下面的c++代码创建了一个全局静态:

static MyType staticType;

Q_GLOBAL_STATIC相比,假设MyType是一个具有构造函数、析构函数或其他非POD的类或结构,使用上述方法有以下缺点:

  • 它需要加载时初始化MyType(也就是说,MyType的默认构造函数在库或应用程序加载时被调用)。

  • 即使在应用程序中从未使用过该类型,它也会被初始化。

  • 不同的编译器翻译单元之间的初始化和销毁顺序不确定,可能会在对象初始化之前或在销毁之后使用,这一点很容易引发运行错误。

Q_GLOBAL_STATIC宏为了解决上述所有问题,会在第一次使用时保证线程安全的初始化并允许查询类型是否已经被销毁,以避免销毁后被再次使用的问题。

三、构造函数和析构函数

对于Q_GLOBAL_STATIC,类型Type必须是公共默认可构造的和公共可销毁的。而对于Q_GLOBAL_STATIC_WITH_ARGS(),必须有一个与传递的参数匹配的公共构造函数。

不能将Q_GLOBAL_STATIC用于具有受保护或私有默认构造函数或析构函数的类型(而对于Q_GLOBAL_STATIC_WITH_ARGS(),则不能用于一个与参数匹配的受保护或私有构造函数)。如果类型具有受保护的成员,则可以通过从该类型派生并创建公共构造函数和析构函数来解决该问题。

例如,以下代码:

class MyType : public MyOtherType { };
Q_GLOBAL_STATIC(MyType, staticType)

上述代码中,MyType的析构函数是隐式成员,如果没有定义其他构造函数,则默认构造函数也是隐式成员。然而,要使用Q_GLOBAL_STATIC_WITH_ARGS(),则需要一个合适的构造函数体:

class MyType : public MyOtherType
{
public:
    MyType(int i) : MyOtherType(i) {}
};
Q_GLOBAL_STATIC_WITH_ARGS(MyType, staticType, (42))

或者,如果编译器支持C++ 11继承构造函数,可以这样写:

class MyType : public MyOtherType
{
public:
    using MyOtherType::MyOtherType;
};
Q_GLOBAL_STATIC_WITH_ARGS(MyType, staticType, (42))
四、Q_GLOBAL_STATIC宏放置的位置

本小节,描述在设计程序时Q_GLOBAL_STATIC宏的放置位置,Q_GLOBAL_STATIC宏会在全局范围内创建了一个静态的类型,不能在函数中放置Q_GLOBAL_STATIC宏,这样做会导致编译错误。

更重要的是,这个宏应该放在源文件中,而不是放在头文件中。由于生成的对象具有静态链接,如果宏被放置在头文件中并被多个源文件包含,则该对象将被定义多次,并且不会导致链接错误。相反,每个翻译单元将引用不同的对象,这可能会导致微妙且难以跟踪的错误。

注意,不建议将宏用于POD类型或具有c++ 11构造函数的类型。对于这些类型,仍然建议使用常规静态,无论是全局的还是局部的函数,这个宏可以工作,但是会增加不必要的开销。

五、构造上的重入性、线程安全、死锁和异常安全

从上文描述可知,Q_GLOBAL_STATIC宏创建了一个对象,该对象在第一次使用时以线程安全的方式初始化:如果多个线程同时尝试初始化这个对象,那么只有一个线程将执行对象初始化操作,而其他线程将等待完成。

如果初始化过程中抛出异常,则认为初始化未完成,并在下一次访问这个对象时再次尝试初始化,如果有线程等待初始化这个对象,那么该线程将被唤醒并尝试初始化。

该宏不保证同一线程的可重入性。如果从构造函数内部直接或间接访问全局静态对象,则肯定会发生死锁。

此外,如果在两个不同的线程上初始化两个Q_GLOBAL_STATIC对象,并且每个对象的初始化序列都访问另一个对象,则可能会发生死锁。因此,需要保持全局静态构造函数的简单性,如果做不到这一点,则应确保在构造过程中对全局静态的使用不存在交叉依赖性。

六、析构

如果该对象在程序的生命周期中从未被使用过,除了QGlobalStatic::exists()QGlobalStatic::isDestroyed()函数外,不会创建Type类型的内容,也不会有任何退出时操作。

如果创建了对象,将在退出时销毁这个队形,类似于C的atexit函数。事实上,在大多数系统中,如果在退出前从内存中卸载库或插件,也会调用析构函数。

然而,可重入性是允许的:在销毁过程中,可以访问全局静态对象,并且返回的指针将与销毁开始前相同。销毁完成后,这时候则不能再访问全局静态对象了。

七、再谈Q_GLOBAL_STATIC

本小节从源码角度来看看Q_GLOBAL_STATIC,本质上Q_GLOBAL_STATIC是一个宏定义,定义在/qtbase/src/corelib/gloabal/qglobalstatic.h文件中,如下代码:

template <typename T, T *(&innerFunction)(), QBasicAtomicInt &guard>
struct QGlobalStatic
{

    typedef T Type;

    bool isDestroyed() const return guard.loadRelaxed() <= QtGlobalStatic::Destroyed; }
    bool exists() const return guard.loadRelaxed() == QtGlobalStatic::Initialized; }
    operator Type *() { if (isDestroyed()) return nullptrreturn innerFunction(); }
    Type *operator()() if (isDestroyed()) return nullptrreturn innerFunction(); }
    Type *operator->()
    {
      Q_ASSERT_X(!isDestroyed(), "Q_GLOBAL_STATIC""The global static was used after being destroyed");
      return innerFunction();
    }
    Type &operator*()
    {
      Q_ASSERT_X(!isDestroyed(), "Q_GLOBAL_STATIC""The global static was used after being destroyed");
      return *innerFunction();
    }
};

#define Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ARGS)                         \
    namespace { namespace Q_QGS_ ## NAME {                                  \
        typedef TYPE Type;                                                  \
        QBasicAtomicInt guard = Q_BASIC_ATOMIC_INITIALIZER(QtGlobalStatic::Uninitialized); \
        Q_GLOBAL_STATIC_INTERNAL(ARGS)                                      \
    } }                                                                     \
    static QGlobalStatic<TYPE,                                              \
                         Q_QGS_ ## NAME::innerFunction,                     \
                         Q_QGS_ ## NAME::guard> NAME;


#define Q_GLOBAL_STATIC(TYPE, NAME)                                         \
    Q_GLOBAL_STATIC_WITH_ARGS(TYPE, NAME, ())


从上述代码可知,Q_GLOBAL_STATIC是一个没有ARGS参数的Q_GLOBAL_STATIC_WITH_ARGS,例如以下代码:

class MyType : public MyOtherType { };
Q_GLOBAL_STATIC(MyType, staticType)

本质上则变成:

    namespace { namespace Q_QGS_staticType {                                  \
        typedef MyType Type;                                                  \
        QBasicAtomicInt guard = Q_BASIC_ATOMIC_INITIALIZER(QtGlobalStatic::Uninitialized); \
        Q_GLOBAL_STATIC_INTERNAL(ARGS)                                      \
    } }

 static QGlobalStatic<MyType, Q_QGS_staticType::innerFunction, Q_QGS_staticType::guard> staticType;

所以,最终则创建一个QGlobalStatic类型的静态模板,从以下图片中代码可知:如果对象已经被销毁,则返回nullptr,否则会返回innerFunction()函数的返回值(指向TYPE的指针):

innerFunction()实现如下:

从上述代码可知,在没有初始化的情况下,对象类型由:

d = new Type ARGS; 

创建,最后并返回d,即返回指向TYPE的指针。

推荐阅读