只有头的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("%dn",*(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("%dn",*(singleton()));
*(singleton()) = 5;
/* Output modified value - expected output: 5 */
printf("%dn",*(singleton()));
/* Output the same value from another module - expected output: 5*/
side();
return 0;
}

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

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

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

我也可以接受任何其他形式的单例实现,它在gcc和MSVC中都是从直C中的仅头库工作的。

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

By;全局";,我认为你的意思是";具有静态存储持续时间和外部链接";。至少,这是C所能达到的。这也与C所能达到的"0"一样接近;"singleton";是内置类型的,因此在这个意义上,术语";全局单例";是多余的。

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

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

另一方面,内联函数方法不太可能在许多C实现中工作。特别要注意的是,inline语义在C中与C++中有很大不同,尤其是外部内联函数在C中很少有用

  • 第6.7.4/3段(语言约束):

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

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

  • 第6.9/5段:

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

    请注意,尽管具有外部链接的函数标识符的内联定义(如您的内联定义)提供了该标识符的外部声明,但它不提供该标识符的内部定义。这意味着程序中的某个位置需要单独的外部定义(除非该函数完全未使用)。此外,该外部定义不能在包含内联定义的翻译单元中。这是外部内联函数在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.h(第一次)之前定义SING_MASTER的一个翻译单元将提供所需的singleton的定义,而所有其他翻译单元将只有一个声明。此外,变量将可以直接访问,而无需调用函数或取消引用指针。

最新更新