【问题标题】:Corrupted singleton data using CxxTest使用 CxxTest 损坏的单例数据
【发布时间】:2010-08-31 16:38:30
【问题描述】:

这是一个奇怪的问题,我不知道该怎么做。

我有类似以下的内容:

struct Parms
{
    const std::string value1;
    const std::string value2;

    std::string parm1;
    std::string parm2;

    Parms() : parm1(value1), parm2(value1) {}

    static const Parms& getDefaults()
    {
        static Parms defaults;
        return defaults;
    }
};

我通常这样使用:

Parms myParms = Parms::getDefaults();
myParms.parm1 = "crap";
functionThatNeedsParms(myParms);

非常简单。这从来没有让我头疼,直到我开始尝试使用 CxxTest 编写使用此代码的单元测试。我在不同的文件中有两个测试套件类,当我单独运行它们时,一切都很好。

当我将它们一起运行时,我看到了两件坏事。首先,整个核心转储试图双重释放静态默认变量。其次,如果我在默认值死前一段时间查看它的内容,但在我开始使用它之后,其中的静态 const std::strings 已损坏(一些字母随机更改,尽管它始终是每次运行都一样)。

发生了什么事?

【问题讨论】:

  • 是不是和上面的代码一样(即getDefaults是隐式内联的,因为在类内部定义)。参数是否内置到一个库中,所有测试代码是否内置到另一个库中? value1/2 真的只是 const 成员,而不是引用或静态或其他奇怪的东西吗?
  • 'defaults' 不是一个好名字。它非常类似于“默认”,这是一个 C++ 关键字
  • 为什么帖子说单例?看起来这个类正在做更多的事情,而不仅仅是“单例”功能。

标签: c++ static singleton coredump


【解决方案1】:

双重释放和核心转储

我想我可以解释您遇到的“双重释放和核心转储”问题。我最近遇到了同样的事情,听起来你正在做和我一样的事情。

根据您的描述,您说当您“单独运行它们”时它们可以正常工作,但如果您“一起运行它们”,则会出现双重释放/核心转储问题。

我发现如果同一个全局被声明了两次,就会发生这种情况。

在我的例子中,我有类 foo,在一个文件中我有一个全局 class foo gFoo;,而在另一个文件中我有一个全局 class foo gFoo;。 (是的,这听起来很愚蠢,实际上我正在链接一个文件 X.cxx 以及一个还包含 X.cxx 的共享库——结果基本相同。)

现在,我本来希望编译器会对此提出投诉,但显然有一些标志可以启用或禁用此检查,并且代码编译得很好。但是当程序终止并调用它的所有析构函数时,它调用了 gFoo 的析构函数两次,并给了我双重释放消息以及核心转储。

鉴于您说它独立工作正常但组合时失败,我敢打赌您将全局定义在两个单独的文件中,并且当它们自己编译时它工作正常,但是当您将它们组合成单个文件时测试,你可能有两次全局声明。

检查一下。

【讨论】:

    【解决方案2】:

    C 和 C++ 中的静态变量不是线程安全的。这意味着如果两个线程尝试访问您的单例对象,可能会发生竞争条件(坏事)。解决问题的一种方法是使用线程本地存储。这由 pthreads 库支持,并且一些编译器直接支持线程本地存储。

    如果您的单例必须对所有线程都是全局的,则另一种方法是提供锁以确保一次只有一个线程可以访问您的数据。

    然而,问题只出现在单元测试中。我建议不要运行多线程单元测试,除非您打算在多个线程中使用单例。

    【讨论】:

    • 我在问题中没有看到任何对多线程环境的引用,所以这个答案正在抓住稻草。在提供答案之前,您应该对询问多线程的问题发表评论。
    • 我从问题的性质和“一起运行”这句话猜测这实际上是多线程。我可能是错的,但问题缺乏细节,这可能是问题所在。
    【解决方案3】:

    这高度依赖于您使用的编译器和平台,如果没有实际看到测试,我只能猜测发生了什么。

    我在您的代码中看到了一些误解:
    1)您缺少复制运算符和复制构造函数 您正在复制包含 std::string 的实例,这可能使用引用计数来实现。引用计数在std::string 的重载复制构造函数/运算符中实现,但这些可能不会从您的类的隐式生成的复制构造函数中调用,因此会导致双重释放内存和其他讨厌的事情。复制运算符/构造函数应如下所示:

    // Copy constructor
    Parms(const Parms& oth)  { parm1 = oth.parm1; parm2 = oth.parm2; }
    
    // Copy operator
    Parms& operator= (const Parms& oth)  { 
      if (&oth == this)  // Check for self-assignment
        return *this;
      parm1 = oth.parm1;
      parm2 = oth.parm2;
      return *this;
    }
    



    2) 我不太了解value1value2 的存在。似乎您从未初始化它们,您只是在默认构造函数中使用它们将它们的(空)内容复制到parm1parm2。您可以完全避免这种情况,因为 parm1parm2 在调用时会自动初始化为空字符串。

    3) 这里不需要使用单例模式。 getDefaults() 方法可以如下实现:

    static Parms getParms() { return Parms(); }
    

    单例模式适用于在整个程序运行过程中只有一个实例的类,而您的类似乎并非如此。

    这样您就可以安全地从多个线程中使用getParms() 函数,并且智能编译器将优化隐含的附加副本。

    【讨论】:

    • 对不起,我应该在我原来的问题中更明确。这是我在工作中遇到的一个问题,所以我不打算复制和粘贴实际代码,所以我放在一起的是(一个显然不是那么清楚的)琐碎的例子。使用单例行为是因为还有一个 setDefaults() 方法,因此您可以设置一次,然后在任何地方重用它们。无论如何,真正的问题是#1。我没有意识到 std::string 实现使用了引用计数,所以我假设浅拷贝是有效的。感谢您的回复。
    • 其实等一下。为什么它被引用会炸毁一切?如果我调用 getDefaults() 并将其分配给某种类型的 Parms,它应该只是增加引用计数,然后当测试结束并且超出范围时,它应该减少它,并且生活应该照常进行.
    • 哦,我明白你在说什么。是否有一些明确的规范可以确定所有可能的复制构造函数和赋值运算符是什么?我觉得很奇怪,他们设计的东西会让你不小心使引用计数无效。
    • 是的,如果默认复制操作符调用相应的字符串复制操作符,这将起作用。尽管应该是这种情况(根据stackoverflow.com/questions/2009996/…),但我发现一些编译器行为不端并执行了相当于 memcpy() 的操作(我认为 Visual C++ 6 做到了,而且很难追踪它)。跨度>
    • 1) 不需要复制构造函数或赋值运算符,因为该类不管理资源,因此默认值非常安全。 2)即使使用了引用计数; std::string 永远不会导致双重删除。 3)这是一个可怕的赋值运算符(它不提供强大的异常保证(请查找Copy and Swap Idiom))
    猜你喜欢
    • 2021-02-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-21
    • 1970-01-01
    • 2015-06-30
    • 1970-01-01
    • 2021-01-03
    • 2021-10-30
    相关资源
    最近更新 更多