我有一个singleton类,用于一个线程(GUI线程(,为了防止错误使用,我添加了assert
//header file
class ImageCache final {
public:
ImageCache(const ImageCache &) = delete;
ImageCache &operator=(const ImageCache &) = delete;
static ImageCache &instance()
{
static ImageCache cache;
return cache;
}
void f();
private:
QThread *create_context_ = nullptr;
ImageCache();
};
//cpp
ImageCache::ImageCache()
{
create_context_ = QThread::currentThread();
qInfo("begin, cur thread %pn", create_context_);
}
void ImageCache::f()
{
assert(create_context_ == QThread::currentThread());
}
虽然一切正常,但在一台机器上,ImageCache::f
中存在断言失败,我无法直接访问该机器(因此存在此问题(。
有趣的是,根据日志ImageCache::ImageCache
根本没有被调用,并且由于而断言失败
assert(0 == QThread::currentThread());
我将CCD_ 5的实现从头文件移动到CCD_,将更新后的源代码发送给这些机器的用户(在我看来一切都很好(,他按照预期重建并全部开工。
我向他询问编译过的二进制文件(有断言失败和没有(,它们之间唯一的区别是ImageCache::instance
实现的位置,
并比较汇编程序。
ImageInstance::instance().f()
的调用之间没有差异完全并且ImageInstance::instance
、的反汇编程序存在一个差异
失败的一个看起来像这样:
static ImageCache &instance()
4938f: 55 push %rbp
49390: 48 89 e5 mov %rsp,%rbp
49393: 41 54 push %r12
49395: 53 push %rbx
{
static ImageCache cache;
49396: 48 8b 05 bb db 23 00 mov 0x23dbbb(%rip),%rax # 286f58 <_ZGVZN10ImageCache8instanceEvE5cache@@Base-0x2150>
4939d: 0f b6 00 movzbl (%rax),%eax
493a0: 84 c0 test %al,%al
493a2: 0f 94 c0 sete %al
493a5: 84 c0 test %al,%al
493a7: 74 5c je 49405 <_ZN10ImageCache8instanceEv+0x76>
493a9: 48 8b 05 a8 db 23 00 mov 0x23dba8(%rip),%rax # 286f58 <_ZGVZN10ImageCache8instanceEvE5cache@@Base-0x2150>
493b0: 48 89 c7 mov %rax,%rdi
493b3: e8 08 b7 fe ff callq 34ac0 <__cxa_guard_acquire@plt>
好的是这样的:
ImageCache &ImageCache::instance()
{
50c12: 55 push %rbp
50c13: 48 89 e5 mov %rsp,%rbp
50c16: 41 54 push %r12
50c18: 53 push %rbx
static ImageCache cache;
50c19: 0f b6 05 98 94 23 00 movzbl 0x239498(%rip),%eax # 28a0b8 <_ZGVZN10ImageCache8instanceEvE5cache>
50c20: 84 c0 test %al,%al
50c22: 0f 94 c0 sete %al
50c25: 84 c0 test %al,%al
50c27: 74 50 je 50c79 <_ZN10ImageCache8instanceEv+0x67>
50c29: 48 8d 3d 88 94 23 00 lea 0x239488(%rip),%rdi # 28a0b8 <_ZGVZN10ImageCache8instanceEvE5cache>
50c30: e8 cb 3d fe ff callq 34a00 <__cxa_guard_acquire@plt>
区别在于
//bad
mov 0x23dbbb(%rip),%rax
movzbl (%rax),%eax
//good
movzbl 0x239498(%rip),%eax
我解释说,由于某种原因,第一个变量的%eax
寄存器得到了错误的值,因此决定全局对象被初始化,而它没有被初始化。在第二种情况下,一切如预期。
那么是编译器故障(gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0 / amd64 / linux
(还是由于某种原因我应该在.cpp
内部使用ImageCache::实例,或者其他导致差异代码生成的原因,比如某些编译器错误可能导致此失败?代码是用-O0 -std=c++11
和其他一些标志编译的,这些标志是cmake在编译依赖于Qt库的共享库时自动添加的。
我还询问了使用CCD_ 14而不是CCD_,并且用户在第二种情况下看到输出,而在第一种情况下没有输出。
原始答案
据我所知,在头文件中使用该函数的问题是,您可以获得多个定义,然后行为是未指定的。
从本质上讲,编译器可能会生成多个函数instance
,每个编译单元都有一个函数,其中包含该标头,因此,如果在链接时不合并/消除这些函数,则每个函数都有自己的变量。
在windows中,如果我们在多个DLL中编译相同的代码,其中一些变量在每个动态库中重复,我们可能会遇到类似的问题。
然后会发生的情况是,因为每个客户端都有自己的副本,所以一个客户端所做的更改不会被另一个翻译单元(你的问题(或另一个DLL(我的问题(中的另一个客户端看到。
通过将定义移动到源文件,您将获得单个定义,从而避免问题。
在C++中,如果不遵守规范,通常会得到未定义的行为。这取决于程序员知道他在做什么。
更新
正如所指出的,在一篇评论中,根据目前的标准,我的假设可能是错误的。因此,问题也可能是过时的编译器或编译器错误。
正在发生的事情的可能解释:
在许多情况下,当编译器合并重复项时,代码将是相同的,因此选择哪一个不会有任何区别。这里,假设编译器为静态变量分配2个不同的地址(每个编译单元一个(,并以某种方式内联对instance()
的调用,即使用原始变量而不是合并(选择(的变量,它可以解释观察到的行为。
看起来QThread::currentThread()
有问题。它使用了一个模块本地静态对象——我想。如果是这样的话,模块与用户代码的链接顺序会导致行为差异。你试过不同版本的QT吗?我想这个版本的Qt有一个过时的设计——没有使用现代习语,比如Meyer singleton,甚至没有使用古老的漂亮计数器技巧。