【问题标题】:Error deleting std::vector in a DLL using the PIMPL idiom使用 PIMPL 习惯用法删除 DLL 中的 std::vector 时出错
【发布时间】:2016-02-02 13:55:45
【问题描述】:

我有以下代码:

在 DLL1 中:

在 .h 文件中:

class MyClass
{
public:
    MyClass();
private:
    std::string m_name;
};

class __declspec(dllexport) Foo
{
private:
    struct Impl;
    Impl *pimpl;
public:
    Foo();
    virtual ~Foo();
};

struct Foo::Impl
{
    std::vector<MyClass> m_vec;
    std::vector<MyClass> &GetVector() { return m_vec; };
};

在 .cpp 文件中:

Foo::Foo() : pimpl ( new Impl )
{
}

Foo::~Foo()
{
    delete pimpl;
    pimpl = NULL;
}

[编辑]

在 DLL2 中

在.h中

class Bar : public Foo
{
public:
    Bar();
    virtual ~Bar();
};

在 .cpp 中:

Bar::Bar()
{
}

Bar::~Bar()
{
}

在 DLL3 中:

extern "C" __declspec(dllexport) Foo *MyFunc(Foo *param)
{
    if( !param )
        param = new Bar();
    return param;
}

在主应用程序中:

void Abc::my_func()
{
    Foo *var = NULL;
// loading the DLL3 and getting the address of the function MyFunc
    var = func( var );
    delete var;
}

现在,我假设复制构造函数应该是私有的,因为复制 Foo 和 Bar 对象没有意义。

现在我的问题是:Bar​​ 是否也应该有复制构造函数和赋值运算符? [/编辑]

注意 MyClass 没有被导出,也没有析构函数。

这通常是你编写代码的方式吗?

问题是我在 Windows 上崩溃了(8.1 + MSVC 2010,如果重要的话)。 如果需要,我可以发布更多代码,但现在只是想确保我没有做明显错误的事情。

崩溃发生在我退出 Base 析构函数并且堆栈跟踪显示:

ntdll.dll!770873a6() [下面的帧可能不正确和/或 丢失,没有为 ntdll.dll 加载符号] ntdll.dll!7704164f()
ntdll.dll!77010f01() KernelBase.dll!754a2844()
dll1.dll!_CrtIsValidHeapPointer(const void * pUserData) 行 2036 C++ dll1.dll!_free_dbg_nolock(void * pUserData,int nBlockUse) 第 1322 行 + 0x9 字节 C++ dll1.dll!_free_dbg(void * pUserData, int nBlockUse) 第 1265 行 + 0xd 字节 C++ dll1.dll!operator delete(void * pUserData) 第 54 行 + 0x10 字节 C++ dll1.dll!Foo::`向量删除析构函数'() + 0x65 字节 C++

谢谢。

更新:

即使我将以下代码放入

extern "C" __declspec(dllexport) Foo *MyFunc(Foo *param)
{
    param = new Bar();
    delete param;
    return param;
}

程序在同一个地方的delete param操作还是crash。

看起来 std::vector 的析构函数是在调用 Foo 的析构函数之后调用的。是不是应该是这样的?

更新2:

在调试器下仔细运行后,我发现崩溃发生在“void operator delete(void *pUserData);”内部。 pUserData 指针的地址为“param”。

DLL1 是用这个构建的:

C++

/ZI /nologo /W4 /WX- /Od /Oy- /D "WIN32" /D "_D​​EBUG" /D "_LIB" /D "_UNICODE" /D "UNICODE" /Gm /EHsc /RTC1 /MTd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fp"Debug\dll1.pch" /Fa"Debug\" /Fo"Debug\" /Fd"Debug\vc100.pdb" /Gd /analyze- /errorReport:queue

图书管理员 /OUT:"C:\Users\Igor\OneDrive\Documents\dbhandler1\docview\Debug\dll1.lib" /NOLOGO

DLL2 是用以下方式构建的:

C++

/I"..\dll1\" /Zi /nologo /W4 /WX- /Od /Oy- /D "WIN32" /D "_USRDLL" /D "DLL_EXPORTS" /D "_DEBUG" /D "_CRT_SECURE_NO_DEPRECATE=1" /D "_CRT_NON_CONFORMING_SWPRINTFS=1" /D "_SCL_SECURE_NO_WARNINGS=1" /D "_UNICODE" /D "MY_DLL_BUILDING" /D "_WINDLL" /D "UNICODE" /Gm- /EHsc /RTC1 /MTd /GS /fp:precise /Zc:wchar_t /Zc:forScope /GR /Fp"vc_mswud\dll2\dll2.pch" /Fa"vc_mswud\dll2\" /Fo"vc_mswud\dll2\" /Fd"vc_mswud\dll2.pdb" /Gd /analyze- /errorReport:queue 

Linker

/OUT:"..\myapp\vc_mswud\dll2.dll" /INCREMENTAL /NOLOGO /LIBPATH:"..\docview\Debug\" /DLL "dll1.lib" "kernel32.lib" "user32.lib" "gdi32.lib" "comdlg32.lib" "winspool.lib" "winmm.lib" "shell32.lib" "shlwapi.lib" "comctl32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "rpcrt4.lib" "advapi32.lib" "version.lib" "wsock32.lib" "wininet.lib" /MANIFEST /ManifestFile:"vc_mswud\dll2\dll2.dll.intermediate.manifest" /ALLOWISOLATION /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /DEBUG /PDB:"vc_mswud\dll2.pdb" /PGD:"C:\Users\Igor\OneDrive\Documents\myapp\dll2\vc_mswud\dll2.pgd" /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"vc_mswud\dll2.lib" /MACHINE:X86 /ERRORREPORT:QUEUE 

有人发现我的库的构建方式有任何问题吗?

【问题讨论】:

  • 我希望你永远不要复制Foo,因为编译器生成的复制构造函数会执行pimpl的浅拷贝,导致双重删除。
  • 你没有遵循三法则。现在,除此之外,我认为您应该展示您如何使用Foo。目前尚不清楚您是否遵循内存分配和 DLL 释放指南。具体来说,分配Foo 的人应该释放Foo。是这样吗?
  • 这不是问题,但是在Foo 的析构函数中将pimpl 设置为NULL 是没有意义的。对象正在消失,所以pimpl 也会消失。
  • 您可能需要发布更多代码。您发布的代码中没有Base,因此退出其析构函数的失败很难解决。
  • 我使用发布的代码创建了一个小项目并且没有崩溃。意味着您与我们共享的代码中可能没有其他内容。顺便说一句,我向您推荐相同的(根据您的帖子制作小项目)。也许它会帮助您更快地发现问题。

标签: c++ dll pimpl-idiom


【解决方案1】:

如果没有可用于分析的进一步代码,我在您发布的代码中看到的一个重要错误是您的 Foo 类是违反所谓的 Rule of Three 的资源管理器。

基本上,您使用newFoo 构造函数中动态分配一个Impl 实例,您有一个用于Foo 的虚拟析构函数使用delete 释放托管资源(pimpl),但是您的@ 987654329@ 类容易被复制
实际上,编译器生成的复制构造函数和复制赋值运算符执行成员方式的复制,它们基本上是pimpl 指针数据成员的浅拷贝:这是“泄漏”的来源".

您可能希望为 Foo 声明 private 复制构造函数和复制赋值,以禁用编译器生成的按成员复制操作:

// Inside your Foo class definition (in the .h file):
...

// Ban copy
private:
    Foo(const Foo&); // = delete
    Foo& operator=(const Foo&); // = delete

注意: C++11 的 =delete 禁用副本的语法在 MSVC 2010 中不可用,因此我将其嵌入到 cmets 中。


与您的问题没有直接关系,但可能值得注意:

  1. 在您的Foo::Impl 结构中,由于m_vec 数据成员已经是public,我认为没有直接的理由提供像GetVector() 这样的访问器成员函数。

  2. 从 C++11 开始,考虑在代码中使用 nullptr 而不是 NULL

【讨论】:

  • 谢谢。我回家后会试试的。有的话会在这里发帖。另外,它们必须是私有的吗?
  • @Igor:不确定我是否理解您的问题。无论如何,正如已经写过的那样,声明私有复制构造函数和赋值运算符会禁用调用自动编译器生成的虚假浅拷贝。
  • 我添加了复制构造函数存根和 operator= 存根返回 *this 作为私有成员,但它仍然崩溃。也许他们应该公开并真正实施?
  • 请查看更新。在同一个函数中调用 new 然后 delete 仍然会导致它崩溃。
  • 您应该为崩溃提供一个简单的可编译重现,以使其成为正确的 StackOverflow 问题。就现在而言,在我看来,这更像是一种咨询请求,如果没有适当的预算,我就无法做到这一点。其他可用带宽比我多的人可能会提供帮助。请注意:在您的最后一个问题更新中,您有一个 MyFunc 正文和 ...delete param; return param;。这是一个错误农场,因为您返回一个悬空(已删除)指针。您可能需要有人能够指导您解决一些基本的 C++ 和 DLL 问题。此外,该错误可能在您未显示的代码中。祝你好运。
【解决方案2】:

问题是您在 DLL3 中分配了 Bar,其中包括 Foo 的包含实例。但是,您通过 Foo* 在主应用程序中删除了它,它已在 DLL1 中完成删除(如您的堆栈跟踪中所示)。

调试堆检查器发现您在一个模块中分配内存并在另一个模块中释放它。


问题的详细解释

调用new Foo(args...) 大致执行以下操作:

pFoo = reinterpret_cast<Foo*>(::operator new(sizeof(Foo)));
pFoo->Foo(args...);
return pFoo;

在 MS Visual Studio C++ 对象模型中,这是在调用 new Foo 时内联的,因此在调用 new 语句时会发生这种情况。

调用delete pFoo 大致执行以下操作:

pFoo->~Foo();
::operator delete(pFoo);

在 MS Visual Studio C++ 对象模型中,这两个操作都被编译成 ~Foo,在 Foo::`vector deleting destructor'() 中,您可以在 Mismatching scalar and vector new and delete 的伪代码中看到。

因此,除非您更改此行为,否则::operator new 将在new Foo站点调用,::operator delete 将在@ 右括号的站点调用987654342@.

我没有在这里详细介绍虚拟或矢量行为,但除了上述之外,它们并没有带来任何进一步的惊喜。

operator newoperator delete 的类特定重载用于代替上面的 ::operator new::operator delete(如果它们存在),这样您就可以控制在哪里调用 ::operator new::operator delete,或者甚至完全调用其他东西(例如池分配器)。这就是您明确解决此问题的方式。

我从MS Support Article 122675 了解到,MSVC++ 5 及更高版本不应该在具有虚拟析构函数的dllexport/dllimport 类的析构函数中包含::operator delete 调用,但我从未设法触发该行为,并且发现明确为 DLL 导出的类分配/取消分配内存的位置更加可靠。


要解决此问题,请给 Foo 类特定的 operator newoperator delete 重载,例如,

class __declspec(dllexport) Foo
{
private:
    struct Impl;
    Impl *pimpl;
public:
    static void* operator new(std::size_t sz);
    static void operator delete(void* ptr, std::size_t sz)
    Foo();
    virtual ~Foo();
};

不要将实现放在标题中,否则会被内联,这会破坏练习的重点。

void* Foo::operator new(std::size_t sz)
{
    return ::operator new(sz);
}

void Foo::operator delete(void* ptr, std::size_t sz)
{
    return ::operator delete(ptr);
}

仅为Foo 执行此操作将导致FooBar 在DLL1 的上下文中被分配和销毁。

如果您希望在 DLL2 的上下文中分配和删除 Bar,那么您也可以给它一个。虚拟析构函数将确保调用正确的operator delete,即使您在给定示例中使用delete 基指针也是如此。不过,您可能需要 dllexport Bar,因为这里的内联程序有时会让您大吃一惊。

有关更多详细信息,请参阅MS Support Article 122675,尽管您实际上已经解决了与他们在那里描述的问题相反的问题。


另一种选择:使Foo::Foo 受保护,Bar::Bar 私有,并从您的 DLL 接口为它们公开静态工厂函数。然后::operator new 调用在工厂函数中而不是调用者的代码中,这将把它放在与::operator delete 调用相同的DLL 中,并且您获得与提供特定于类的operator newoperator delete 相同的效果,以及工厂函数的所有其他优点和缺点(一旦您停止传递原始指针并根据您的要求开始使用unique_ptrshared_ptr,这是一个很大的改进)。

要做到这一点,你必须相信Bar 中的代码不会调用new Foo,否则你已经把问题带回来了。因此,按照惯例,这种保护更加严格,而特定于类的operator new/operator delete 表示要求以某种方式完成该类型的内存分配。

【讨论】:

  • 请查看更新。即使我分配内存然后在指针上调用 delete 它仍然崩溃。
  • 如果堆栈跟踪没有改变,那么你仍然在 DLL3 中分配,在 DLL1 中删除。在调用new 的位置分配内存,并在定义~Foo 的位置删除内存。将delete 移动到哪里都没有关系。
  • 为什么delete()函数中多了一个“size”参数?而 IIUC,加上这一点,所有的内存管理都将在 DLL1/DLL2 中进行,对吧?
  • 谢谢。将构造函数/析构函数添加到 Foo 后,它工作并且没有崩溃。
  • 我说得太快了,当我将 new 和 delete 放在一个函数中时它起作用了。但是拆分后又崩溃了。看起来指针不好。我想我应该在主应用程序中设置 Foo 类并尝试这种方式。
猜你喜欢
  • 1970-01-01
  • 2021-09-12
  • 1970-01-01
  • 2011-06-22
  • 2017-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-27
相关资源
最近更新 更多