仅标头 C 库中的全局单例

问题描述

我正在尝试在 C(不是 C++)的头文件库中实现一个全局单例变量。所以在这个论坛和其他地方搜索之后,我发现了 Meyer 的单例的一个变体,我在这里适应了 C:

    /* File: sing.h */
    #ifndef SING_H
    #define SING_H
    inline int * singleton()
    {
        static int foo = 0;
        return &foo;
    }
    #endif

请注意,我正在返回一个指针,因为 C 缺少 C++ 中可用的引用,所以我必须解决它。

好的,现在我要测试它,所以这里是一个简单的测试代码

    /* File: side.h */
    #ifndef SIDE_H
    #define SIDE_H
    void side();
    #endif
    /*File: side.c*/
    #include "sing.h"
    #include <stdio.h>
    void side()
    {
        printf("%d\n",*(singleton()));
    }
    /*File: main.c*/
    #include "sing.h"
    #include "side.h"
    #include <stdio.h>
    int main(int argc,char * argv[])
    {
        /* Output default value - expected output: 0 */
        printf("%d\n",*(singleton()));
        *(singleton()) = 5;
        /* Output modified value - expected output: 5 */
        printf("%d\n",*(singleton()));
        /* Output the same value from another module - expected output: 5*/
        side();
        return 0;
    }

在 C 模式下在 MSVC 中编译并运行良好(也在 C++ 模式下,但这不是主题)。但是,在 gcc 中它输出两个警告(警告:'foo' 是静态的,但在非静态的内联函数 'singleton' 中声明),并生成一个可执行文件,然后当我尝试运行它时出现段错误.警告本身对我来说有点道理(事实上,我很惊讶我没有在 MSVC 中得到它),但是段错误暗示 gcc 永远不会将 foo 编译为静态变量的可能性,使其成为局部变量堆栈,然后返回该变量的过期堆栈地址。

我尝试将单例声明为 extern inline,它在 MSVC 中编译并运行良好,导致 gcc 中出现链接错误(同样,我不抱怨链接错误,这是合乎逻辑的)。 我也试过 static inline(在 MSVC 和 gcc 中都编译得很好,但可以预见的是在第三行运行时输出错误,因为 side.c 翻译单元现在有自己的单例副本。

那么,我在 gcc 中做错了什么?我在 C++ 中没有这些问题,但是在这种情况下我不能使用 C++,它必须是直接的 C 解决方案。

我还可以接受任何其他形式的单例实现,这些实现可以在 gcc 和 MSVC 中的直接 C 中从仅标头库中工作。

解决方法

我正在尝试在 C(不是 C++)的头文件库中实现一个全局单例变量。

“全局”是指“具有静态存储持续时间和外部链接”。至少,这是尽可能接近 C。这也与 C 可以接近内置类型的“单例”一样接近,因此从这个意义上说,术语“全局单例”是多余的。

请注意,我正在返回一个指针,因为 C 缺少 C++ 中可用的引用,所以我必须解决它。

C 没有引用是正确的,但如果您不使用函数来包装对对象的访问,则不需要指针或引用。我并没有真正看到你试图通过它获得什么。您可能会发现没有您想要的东西会更容易。例如,当面对相同变量标识符的重复外部定义时,除了最新版本的 GCC 之外,所有其他版本的默认行为是将它们合并为一个变量。尽管当前的 GCC 将这种情况报告为错误,但通过打开命令行开关仍然可以使用旧的行为。

另一方面,您的内联函数方法不太可能在许多 C 实现中工作。请特别注意,C 中的 inline 语义与 C++ 中的语义大不相同,尤其是外部内联函数在 C 中很少有用。请考虑 C 标准的这些规定:

  • 第 6.7.4/3 段(语言限制):

    具有外部链接的函数的内联定义不应包含具有静态或线程存储持续时间的可修改对象的定义,并且不应包含对具有内部链接的标识符的引用。 >

    因此,您的示例代码不符合标准,需要符合标准的编译器进行诊断。尽管如此,他们可能会接受您的代码,但他们可以用它做任何他们选择的事情。期望您可以依靠随机符合的 C 实现来接受您的函数代码并编译它,以便不同翻译单元中的调用者可以通过调用该函数获得指向同一对象的指针,这似乎是不合理的希望。

  • 第 6.9/5 段:

    外部定义是一个外部声明,它也是一个函数(内联定义除外)或对象的定义。如果在表达式 [...] 中使用了通过外部链接声明的标识符,则在整个程序的某处,该标识符应该只有一个外部定义 [...]。

    请注意,尽管具有外部链接的函数标识符的内联定义(例如您的)提供了该标识符的外部声明,但它不提供外部定义 它。这意味着在程序中的某处需要一个单独的外部定义(除非该函数完全未使用)。此外,该外部定义不能位于包含内联定义的翻译单元中。这是 extern inline 函数在 C 中很少有用的主要原因之一。

  • 第 6.7.4/7 段:

    对于具有外部链接的函数,以下限制适用:[...] 如果翻译单元中函数的所有文件范围声明都包含不带 extern 的内联函数说明符,则该翻译单元是一个内联定义。 内联定义不为函数提供外部定义,并且不禁止在另一个翻译单元中的外部定义。内联定义提供了外部定义的替代方案,翻译器可以使用它来实现对同一翻译单元中的函数的任何调用。 未指定对函数的调用是使用内联定义还是外部定义。

    除了呼应 6.9/5 的部分内容之外,它还警告您,如果您确实提供了与内联定义一起使用的函数的外部定义,则您无法确定哪个将用于为任何特定调用提供服务。

  • 此外,您无法通过声明具有内部链接的函数来解决这些问题,因为虽然这将允许您在其中声明一个静态变量,但该函数的每个定义都是不同的函数。为免有任何疑问,脚注 140 澄清了在这种情况下,

    由于内联定义不同于对应的外部定义以及其他翻译单元中的任何其他对应内联定义,所有具有静态存储持续时间的对应对象在每个定义中也不同

(添加了强调。)

同样,您的示例中介绍的方法不能依赖于在 C 中工作,尽管您可能会发现在实践中,它确实适用于某些编译器。


如果您需要将其作为仅头文件库,那么您可以通过向用户提出额外要求以可移植的方式实现它:使用头文件库的任何程序中的一个翻译单元必须在之前定义一个特殊的宏包括标题。例如:

/* File: sing.h */
#ifndef SING_H
#define SING_H

#ifdef SING_MASTER

int singleton = 0;

#else

extern int singleton;

#endif
#endif

这样,在包含 SING_MASTER(第一次)之前定义 sing.h 的一个翻译单元将提供所需的 singleton 定义,而所有其他翻译单元将只有声明。此外,该变量将可以直接访问,无需调用函数或取消引用指针。