首页 > 解决方案 > 宏是否可以扩展为在程序启动时仅执行一次的代码,而不管它使用了多少次?

问题描述

在我的 Qt 应用程序中,我使用预处理器宏来自动声明和注册元类型:

#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE) \
    Q_DECLARE_METATYPE(TYPE) \
    static struct TYPE ## _metatype_registrar { \
        TYPE ## _metatype_registrar() { \
            qRegisterMetaType<TYPE>(); \
        } \
    } _ ## TYPE ## _metatype_registrar;

我通常在定义一个我想与 Qt 的元类型系统一起使用的类型之后在头文件中使用它,例如:

struct MyDataType {
    int foo;
    double bar;
};

Q_DECLARE_AND_REGISTER_METATYPE(MyDataType)

它之所以有效,是因为它定义的静态结构实例将始终在程序启动时调用其构造函数。我更喜欢这种方法,因为现在我不需要在程序开始时在我的代码中的某处分别注册我关心的每种类型。(我曾经使用这种方法,但我经常忘记在那里添加新类型,因为我健忘,导致恼人的运行时错误。)

但是,使用此宏的一个问题是,对于每个包含相应使用Q_DECLARE_AND_REGISTER_METATYPE.

从技术上讲,Qt 允许qRegisterMetaType多次调用同一类型,并忽略后续调用。所以这不会导致任何错误。但这仍然让我觉得效率低下且不干净。每个包含头文件的源文件(直接或间接使用)Q_DECLARE_AND_REGISTER_METATYPE都会将其扩展为定义一个单独的静态结构的代码,该结构的构造函数将在启动时执行,用大量冗余结构和函数乱扔编译代码,所有这些都将是执行。

我想看看我是否可以改进它,以便Q_DECLARE_AND_REGISTER_METATYPE对特定类型的每次使用都会导致它只在整个程序中执行它扩展为每种类型一次的代码,并且(作为“延伸”目标)不会'不要创建冗余结构,假设这是可能的。

更具体地说,如果头文件foo.h包含 line Q_DECLARE_AND_REGISTER_METATYPE(Foo),文件a.cpp,b.cppc.cpp所有 include foo.h,我希望编译的程序只调用qRegisterMetaType<Foo>();一次。如果可能的话,我希望它不要创建多个冗余实例_Foo_metatype_registrar

如果不是出于效率和清洁的原因,我想这样做我也许可以学习一些有趣的 C++ 预编译器技巧!

有没有办法做到这一点?

标签: c++qtmacrosc-preprocessor

解决方案


这是可以通过单例模式解决的问题。简而言之,单例模式确保只创建一个类型的单个实例。

一种方法是创建一个内部单例类,该类在构造函数中实现您的注册操作。然后,您的外部类将在其构造函数中获取单例实例。无论static创建多少外部类,内部类都是单例的,所以它的构造函数只会被调用一次。把所有这些放在一起,你的宏可以这样重写:

#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE) \
    Q_DECLARE_METATYPE(TYPE) \
    static struct TYPE ## _metatype_registrar { \
        class inner { \
            inner() { qRegisterMetaType<TYPE>(); } \
            inner(const inner &) = delete; \
            void operator=(const inner &) = delete; \
        public: \
            static inner & once () { \
                static inner instance; \
                return instance; \
            } \
        }; \
        TYPE ## _metatype_registrar() { \
            inner::once(); \
        } \
    } _ ## TYPE ## _metatype_registrar;

我确实调查过std::call_once,它确实有效。

#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE) \
    Q_DECLARE_METATYPE(TYPE) \
    static struct TYPE ## _metatype_registrar { \
        TYPE ## _metatype_registrar() { \
            static std::once_flag f; \
            std::call_once(f, qRegisterMetaType<TYPE>); \
        } \
    } _ ## TYPE ## _metatype_registrar;

但是,这需要您#include <mutex>链接到您的线程库,如果您的程序不是多线程的,这可能看起来很奇怪。

请注意,这些将防止发生多次注册。但是,您的解决方法需要通过头文件在翻译单元中定义静态实例,否则您将违反单一定义规则。因此,上面的解决方案并不能解决您的代码被这些冗余静态对象乱扔的问题。


但是,如果您使用模板,则有一种解决方法。C++ 有一个模板类的特殊情况,它会自动将多余的静态成员定义合并到一个定义中。这是因为模板完全在头文件中实现,其中通常也定义了其静态成员。因此,您可以为您的注册商创建一个通用模板类。

template <typename T>
class MetatypeRegistrar {
    MetatypeRegistrar () { qRegisterMetatype<T>(); }
public:
    static MetatypeRegistrar _registrar;
};

template <typename T>
MetatypeRegistrar<T> MetatypeRegistrar<T>::_registrar;

请注意最后两行如何为_registrar.

现在,您可以使用此宏在您的类型的头文件中自动注册:

#define Q_DECLARE_AND_REGISTER_METATYPE(TYPE) \
Q_DECLARE_METATYPE(TYPE) \
template class MetatypeRegistrar<TYPE>;

这本质上是具有提供的类型的模板的显式实例化。

多个源文件可能会创建模板的多个实例,MyDataType因为包含头文件。但是,编译器会自动将静态模板类数据成员的隐含多个实例合并到一个实例中。


推荐阅读