【问题标题】:No call of constructor of singleton没有调用单例的构造函数
【发布时间】: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 的文档。

标签: c++ c++11 assembly


【解决方案1】:

原答案

据我了解,头文件中的函数的问题是您可以获得多个定义,然后行为未指定。

基本上,编译器可能会生成多个函数instance,每个包含该标头的编译单元一个函数,因此如果它们在链接时没有合并/消除,每个函数都会有自己的变量。

在 Windows 中,如果我们在多个 DLL 中编译相同的代码,其中一些变量在每个动态库中重复,我们可能会遇到类似的问题。

然后会发生什么,因为每个客户都有自己的副本,所以另一个客户在另一个翻译单元(您的问题)或另一个 DLL(我的问题)中看不到一个客户所做的更改。

通过将定义移至源文件,您将获得一个定义,从而避免该问题。

在 C++ 中,如果您不遵循规范,您经常会遇到未定义的行为。程序员自己知道自己在做什么。

更新

正如所指出的,在评论中,根据当前标准,我的假设可能是错误的。因此,问题也可能是过时的编译器编译器错误

对正在发生的事情的可能解释:

在许多情况下,当编译器合并重复时,代码将是相同的,因此选择哪一个不会有任何区别。在这里,假设编译器为静态变量分配了 2 个不同的地址(每个编译单元一个)并以某种方式内联对 instance() 的调用,以使用原始变量而不是合并(选定)变量,它可能解释观察到的行为。

【讨论】:

  • C++ 确实保证函数静态变量是唯一的,即使函数是内联的并且它的定义在多个翻译单元中。虽然我不确定 Windows DLL 链接是否能够在所有情况下正确遵守该规则。
  • @aschepler 我已经添加了一些精度,表明它可能是编译器错误……过去,必须在 CPP 文件中声明类静态变量(模板除外),因此编译器可能是没有正确处理这种边缘情况。
【解决方案2】:

QThread::currentThread() 似乎有问题。它正在使用模块本地静态对象-我猜。如果是这种情况,则模块与用户代码的链接顺序会导致行为差异。您是否尝试过不同版本的 QT? 我猜这个版本的 Qt 有一个过时的设计 - 没有使用像 Meyer singleton 这样的现代成语,甚至没有使用旧的漂亮的计数器技巧。

【讨论】:

    猜你喜欢
    • 2018-05-30
    • 2021-09-09
    • 2012-07-11
    • 2013-03-05
    • 2013-05-26
    • 1970-01-01
    • 2019-01-01
    • 1970-01-01
    • 2016-06-28
    相关资源
    最近更新 更多