【问题标题】:Problems with shared_ptr and operator newshared_ptr 和 operator new 的问题
【发布时间】:2020-12-12 23:21:36
【问题描述】:

我在测试代码中遇到了一个错误,我必须测试位于钻石遗产底部的一个类,最重要的是,它与另一个类形成了循环依赖,太难改变了(我保证不是我的代码...)。

类 A 的构造函数请求对类 B 的引用,类 B 请求对类 A 的 shared_ptr...
出于单元测试的目的,我想通过为 A 分配内存但不构建实例来打破这种依赖关系。然后通过将此 shared_ptr 传递给 B,一旦 B 被构建,通过构建 A 实例,其构造函数将引用 B。

我成功地在示例代码中重现了该问题,您可以在本地或任何在线编译器中执行:

#include <iostream>
#include <memory>
using namespace std;

struct ILevel_0
{
    virtual ~ILevel_0() = 0;
};
ILevel_0::~ILevel_0() {}

struct Level_1_A : virtual ILevel_0
{
    int a;
};

struct Level_1_B : virtual ILevel_0
{
    int b;
};

struct Level_2 : Level_1_A, Level_1_B
{
    int c;
};

struct OtherStruct
{
    OtherStruct(const std::shared_ptr<ILevel_0>& lev) : _lev(lev) {}
    std::shared_ptr<ILevel_0> _lev;
};

int main()
{
    std::shared_ptr<Level_2> level2;
    
    void* level2_rawMemory = operator new(sizeof(Level_2));
    Level_2* level2Ptr = static_cast<Level_2*>(level2_rawMemory);
    level2.reset(level2Ptr);
    
    std::cout << "------ 1" << std::endl;
    
    std::shared_ptr<ILevel_0> level0 = level2;  // OK
    
    std::cout << "------ 2" << std::endl;

    // OtherStruct otherStruct(level2);   // KO  : crash after ------ 1
    
    std::cout << "------ 3" << std::endl;

    // Needed, else the shared_ptr's deleter would crash by calling the delete instruction
    new (level2.get()) Level_2{};

    std::cout << "------ 4" << std::endl;
    
    return 0;
}

可以看到一行(构建一个OtherStruct实例)被注释掉了,这样代码代码就可以编译运行而不会崩溃。如果取消注释,程序将崩溃,不打印“----- 2”。我不明白为什么,也不明白为什么前面的指令(level0建设)没有崩溃。

感谢您的帮助,对于粗略的英语表示抱歉。

【问题讨论】:

  • 代码 segvs 对我来说甚至 没有 取消注释该行。这意味着您已经处于未定义的行为世界中,因此恕我直言,搜索注释行有什么问题是毫无意义的,因为其余所有内容都有问题。
  • 您可以在移动到下一行之前刷新std::cout 以打印所有输出
  • @fdermishin,我想std::endl is already flushing
  • 当程序包含未定义的行为时,解释为什么会得到意外结果是没有意义的。此外,几乎没有正当理由直接致电operator new

标签: c++ shared-ptr new-operator diamond-problem


【解决方案1】:

在 ASAN/UBSAN 下运行您的代码:

一步一步发现

39          std::shared_ptr<ILevel_0> level0 = level2; // OK

是罪魁祸首。

然而,真正的罪犯是这样的:

    void* level2_rawMemory = operator new(sizeof(Level_2));
    auto* level2Ptr = static_cast<Level_2*>(level2_rawMemory);
    level2.reset(level2Ptr);

它违反了 C++ 内存模型。您不能只是将原始内存重新解释为 Level_2 对象。特别是当数据类型不是 POD 时(它们不是,因为它们是虚拟的)。

改为使用

    Level_2* level2Ptr = new Level_2;
    level2.reset(level2Ptr);

确实

    level2.reset(new Level_2);

最好的:

std::shared_ptr<Level_2> level2 = std::make_shared<Level_2>();

实际上考虑安全指针转换(参见Using boost::function with a parameter to shared pointer to derived class):

std::shared_ptr<ILevel_0> level0 = dynamic_pointer_cast<ILevel_0>(level2); // OK

带修复的演示

Live On Coliru

#include <iostream>
#include <memory>
#include <utility>

struct ILevel_0 { virtual ~ILevel_0() = default; };

struct Level_1_A : virtual ILevel_0 { int a{}; };
struct Level_1_B : virtual ILevel_0 { int b{}; };

struct Level_2 : Level_1_A, Level_1_B { int c{}; };

struct OtherStruct {
    explicit OtherStruct(std::shared_ptr<ILevel_0> lev) : _lev(std::move(lev)) {}
    std::shared_ptr<ILevel_0> _lev;
};

int main() {
    std::cout << std::unitbuf;
    std::shared_ptr<Level_2> level2 = std::make_shared<Level_2>();

    std::cout << "------ 1" << std::endl;
    auto level0 = std::dynamic_pointer_cast<ILevel_0>(level2); // OK

    std::cout << "------ 2" << std::endl;

    OtherStruct otherStruct(level2);

    std::cout << "------ 3" << std::endl;

    level2 = std::make_shared<Level_2>();

    std::cout << "------ 4" << std::endl;
}

打印

------ 1
------ 2
------ 3
------ 4

而且没有消毒剂警告。

【讨论】:

  • 添加了一个带有修复的演示 Live On Coliru
  • 首先,感谢您的回答和花费的时间。您建议这样做:Level_2* level2Ptr = new Level_2; 但它确实实例化了一个 Level2 对象,正如我在介绍中所说,在我的真实代码中,由于循环依赖,我不能这样做。这就是为什么我只想分配内存,传递指针,然后实例化。这就是我的问题的重点,这就是我使用operator new 的原因。
  • 顺便说一句,如果你知道有关 C++ 内存模型的文档,我想阅读它。
  • 最佳文档在标准中。但是,我很想猜测您来自 C 背景,并且意外地认为 C++ 是“几乎带有类的 C”。这是一个谬论。我会推荐一本像“Accelerated C++”(Moo/Koenig)这样的书。或者,cppreference.com 是一个非常好的语言超链接参考:en.cppreference.com/w/cpp/language/object
  • “我不能这样做,因为循环依赖” - 我无法想象这个问题。您知道您可以在构造函数中使用this,对(某些限制适用于子对象/派生的初始化顺序)。否则,只需使用两步初始化函数(这是一种反模式,但由于您没有描述为什么需要它,也许您这样做了,这给了您怀疑的好处)
猜你喜欢
  • 2012-01-03
  • 2021-10-11
  • 2021-10-28
  • 2020-01-12
  • 1970-01-01
  • 1970-01-01
  • 2020-04-13
  • 2011-03-25
相关资源
最近更新 更多