Qt Q_GLOBAL_STATIC创建全局静态对象

发布时间 2023-07-18 10:40:41作者: 一杯清酒邀明月

概述

  所谓的全局静态对象,大多是在单例类中所见,之前写过一篇文章介绍如何实现一个单例类,在这里,这是最常见的方式来进行创建,需要自定义 static 类对象, 并进行手动初始化。而今天要说的是更简单的方式来实现,Qt 提供了一个非常方便的宏Q_GLOBAL_STATIC,可以快速创建全局静态对象。

QGlobalStatic类

  其实Q_GLOBAL_STATIC宏是在QGlobalStatic中定义的,不过通常都不会直接使用QGlobalStatic类,而是使用类中定义的宏Q_GLOBAL_STATIC,与之对应的还有一个宏Q_GLOBAL_STATIC_WITH_ARGS,后面进行介绍。

基本用法

  首先我们举例看看这个宏才怎么使用:

1 Q_GLOBAL_STATIC(MyType, globalState)
2 QString someState()
3 {
4    if (globalState.exists())
5        return globalState->someState;
6    return QString();
7 }

  这就创建了一个全局静态类对象staticType,MyType是类名,在上面的声明之后,staticType对象可以像使用指针一样使用,保证只能初始化一次。除了用作指针外,对象还提供了两种方法来确定全局的当前状态:exists()和isDestroyed()。

宏定义介绍

Q_GLOBAL_STATIC(Type, VariableName)

  由Q_GLOBAL_STATIC创建的对象在首次使用时进行初始化,它不会增加应用程序或库的加载时间。此外,该对象在所有平台上都以线程安全的方式初始化的。
  这个宏的典型用法如下,在全局上下文中(即,在任何函数体之外):

Q_GLOBAL_STATIC(MyType, staticType)

  这个宏旨在替代不是POD的全局静态对象(普通的旧数据,或者用C ++ 11的术语,不是由平凡的类型组成),因此也就是名称。例如,下面的C ++代码创建一个全局静态:

static MyType staticType;

  与Q_GLOBAL_STATIC相比,假设这MyType是一个具有构造函数,析构函数的类或结构,或者是非POD,上面写法有以下缺点:

  • 它需要加载时初始化MyType(即MyType加载库或应用程序时调用的默认构造函数);
  • 即使从未使用过,类型也会被初始化;
  • 不同的单元之间的初始化和破坏的顺序不确定,导致在初始化之前或销毁之后可能会使用;
  • 如果它是在一个函数内(即不是全局的)发现的,它将在第一次使用时被初始化,但是许多当前编译器中并不保证初始化是线程安全的;

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

  注意:如果要使用该宏,那么累的构造函数和析构函数必须是公有的才行,如果构造函数和析构函数是私有或者受保护的类型,是不能使用该宏的。

  对于具有受保护或私有默认构造函数或析构函数的类型(对于Q_GLOBAL_STATIC_WITH_ARGS(),一个与参数匹配的受保护或私有构造函数),不能使用Q_GLOBAL_STATIC 。如果将这些成员保护起来,可以通过  从类型派生出来并创建公共构造函数和析构函数来解决问题。如果类型将它们设为私有,则派生之前需要声明friend。

  例如,MyType基于先前定义的MyOtherType具有受保护的默认构造函数和/或受保护的析构函数

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

  MyType由于析构函数是隐式成员,因此不需要定义,如果没有定义其他构造函数,则默认构造函数也是如此。但是,与Q_GLOBAL_STATIC_WITH_ARGS()一起使用时,需要一个合适的构造函数体:

1  class MyType : public MyOtherType
2   {
3   public:
4       MyType(int i) : MyOtherType(i) {}
5   };
6   Q_GLOBAL_STATIC_WITH_ARGS(MyType, staticType, (42))

该宏的声明位置

  Q_GLOBAL_STATIC宏在全局范围内创建一个必须是静态的类型。无法将Q_GLOBAL_STATIC宏放在函数中(这样做会导致编译错误)。

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

线程安全,死锁和构建异常安全
  Q_GLOBAL_STATIC宏创建一个对象,它在首次使用时以线程安全的方式初始化自己:如果多个线程同时尝试初始化对象,则只有一个线程会继续初始化,而其他所有线程都会等待完成。

  如果初始化过程抛出异常,则认为初始化未完成,并在控制达到任何对象使用时再次尝试。如果有任何线程在等待初始化,其中一个线程将被唤醒尝试初始化。

  宏不能保证来自同一线程的重入。如果全局静态对象是直接或间接从构造函数中访问的,那么肯定会发生死锁。

  另外,如果两个Q_GLOBAL_STATIC对象正在两个不同的线程上初始化,并且每个初始化序列都访问另一个线程,则可能会发生死锁。出于这个原因,建议保持全局静态构造器简单,否则,确保在构造过程中不使用全局静态的交叉依赖。

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

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

  由于销毁是在程序退出时发生的,因此不提供线程安全性。这包括插件或库卸载的情况。另外,由于析构函数不会抛出异常,因此也不会提供异常安全性。

  但是,重新调用是允许的,在销毁期间,可以访问全局静态对象,并且返回的指针与销毁开始之前的指针相同。销毁完成后,不允许访问全局静态对象,除非在QGlobalStatic API中注明。

Q_GLOBAL_STATIC_WITH_ARGS(Type, VariableName, Arguments)

该宏的用法:

Q_GLOBAL_STATIC_WITH_ARGS(MyType, staticType, (42, "Hello", "World"))

注意:宏参数需要包含在括号中。
除了使用提供的参数实际初始化内容外,该宏的行为与Q_GLOBAL_STATIC()的行为相同。