宏是否有可能扩展为在程序启动时只执行一次的代码,而不管它被使用了多少次?

Is it possible for a macro to expand to code that will be executed exactly once when a program starts, regardless of how many times it's used?

在我的 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 包含行 Q_DECLARE_AND_REGISTER_METATYPE(Foo),并且文件 a.cppb.cppc.cpp 都包括foo.h,我希望编译后的程序只调用 qRegisterMetaType<Foo>(); 一次。如果可能的话,我希望它不要创建 _Foo_metatype_registrar.

的多个冗余实例

如果不是为了效率和整洁的原因,我想这样做我也许可以学到一些有趣的 C++ 预编译技巧!

有什么办法吗?

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

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

#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> 和 link 使用您的线程库,如果您的程序不是多线程的,这可能看起来很奇怪。

请注意,这些将防止发生多次注册。但是,您的解决方法需要 static 实例由头文件在翻译单元中定义,否则您将违反单一定义规则。因此,上述解决方案无法解决您的代码中充斥着这些冗余静态对象的问题。


但是,如果您使用模板,则可以解决这个问题。 C++ 对模板 classes 有一个特例,它会自动将冗余的静态成员定义合并到一个定义中。这是因为模板完全在头文件中实现,其中通常还定义了其静态成员。因此,您可以为您的注册商创建一个通用模板 class。

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 创建模板的多个实例,因为包含头文件。但是,编译器会自动将静态模板 class 数据成员的隐含多个实例合并为一个实例。