【发布时间】:2018-12-18 22:03:04
【问题描述】:
我有单例类,打算在一个线程(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 %p\n", create_context_);
}
void ImageCache::f()
{
assert(create_context_ == QThread::currentThread());
}
一切正常,但在一台机器上,ImageCache::f 中的断言失败,
我无法直接访问那台机器(因此提出了这个问题)。
有趣的是,根据日志ImageCache::ImageCache
根本没有被调用,并且断言失败是因为
assert(0 == QThread::currentThread());
我将ImageCache::instance 的实现从头文件移动到.cpp 文件,
将更新的源代码发送给这些机器的用户(在我的一切正常),
他重建并开始按预期工作。
我向他询问编译后的二进制文件(有断言失败和没有),它们之间的唯一区别是 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::instance,
或其他导致代码生成差异的原因,例如某些编译器错误可能导致此失败?代码是使用-O0 -std=c++11 和一些其他标志编译的,这些标志是 cmake 在编译依赖于 Qt 库的共享库时自动添加的。
我还询问使用fprintf(stderr而不是qInfo的测试代码,
并且用户在第二种情况下看到输出,在第一种情况下没有输出。
【问题讨论】:
-
我假设您看不到
ImageCache::ImageCache的日志,因为它是静态构建的,可能在初始化记录器之前。我建议你以“懒惰”的方式构造单例(持有一个初始化为 null 的静态指针数据成员,并在第一次调用ImageCache::instance()并且指针为空时构造一个实例。考虑 en.wikipedia.org/wiki/Double-checked_locking )。 -
@uv_ 并查看您的参考资料:
C++11 For the singleton pattern, double-checked locking is not needed:,我使用c++11。 -
@uv_ 据我所知,至少对于 C++11 的情况,你是不对的。第一次调用导致对象的构造。这是处理
c++静态对象初始化顺序未指定的问题的技巧。 -
内联
instance函数必须处理静态变量,以及指示它已被初始化的相关布尔标志,这与非内联版本不同。由于每个翻译单元最初都有自己的函数和变量副本,因此生成的代码访问将通过指针而不是直接共享的一个副本。这就是两个“坏”指令正在做的事情。但这并不能解释断言失败。 -
static内联函数中的变量被旧编译器“窃听”。请参阅 gcc -fno-gnu-unique 的文档。