使用PIMPL惯用法删除DLL中的std :: vector时出错

时间:2016-02-02 13:55:45

标签: c++ dll pimpl-idiom

我有以下代码:

在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中

in .h

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

in .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)Line   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 ::`vector删除析构函数'()+ 0x65字节C ++

谢谢。

更新:

即使我在

中加入以下代码
extern "C" __declspec(dllexport) Foo *MyFunc(Foo *param)
{
    param = new Bar();
    delete param;
    return param;
}

程序在同一个地方的删除参数操作中仍然崩溃。

在调用Foo的析构函数之后,看起来后来会调用std :: vector的析构函数。这是它的假设吗?

UPDATE2:

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

DLL1是用这个构建的:

  

C ++

     

/ ZI / nologo / W4 / WX- / Od / Oy- / D“WIN32”/ D“_DEBUG”/ D“_LIB”/ D   “_UNICODE”/ D“UNICODE”/ Gm / EHsc / RTC1 / MTd / GS / fp:精确   / 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 

是否有人发现我的库的构建方式存在任何问题?

2 个答案:

答案 0 :(得分:1)

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

基本上,您使用ImplFoo构造函数中动态分配new实例,您有Foo的虚拟析构函数释放托管资源({{1} })pimpl,但您的delete班级容易受到副本
实际上,编译器生成的复制构造函数和复制赋值运算符执行成员方复制,它们基本上是Foo指针数据成员的浅拷贝:这是的来源&#34; leaktrocities&#34;

您可能需要声明私有复制构造函数和pimpl的复制赋值,以禁用编译器生成的成员复制操作:

Foo

注意: MSVC 2010中没有用于禁用副本的C ++ 11 // Inside your Foo class definition (in the .h file): ... // Ban copy private: Foo(const Foo&); // = delete Foo& operator=(const Foo&); // = delete 语法,因此我将其嵌入到评论中。

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

  1. =delete结构中,由于Foo::Impl数据成员已经是m_vec,因此我认为没有直接的理由提供public之类的访问者成员函数。

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

答案 1 :(得分:1)

问题是您在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 ++对象模型中,这两个操作都编译为~FooFoo::`vector deleting destructor'(),您可以在Mismatching scalar and vector new and delete的伪对象中看到。

因此,除非您更改此行为,否则::operator new会在 new Foo 的网站上调用,::operator delete会在网站上调用~Foo 的右大括号。

我没有详细说明虚拟或矢量行为,但除了上述内容之外,它们不会带来任何进一步的惊喜。

使用operator newoperator delete的特定于类的重载,而不是上面的::operator new::operator delete,如果它们存在,则可以控制{{1}调用和::operator new,甚至完全调用其他东西(例如池分配器)。这就是你明确解决这个问题的方法。

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

要解决此问题,请为operator newoperator delete提供dllimport特定于类的重载,例如,

Foo

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

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); } 执行此操作将导致{1}}和Foo在DLL1的上下文中分配和销毁。

如果你想在DLL2的上下文中分配和删除Foo,那么你也可以给它一个。虚拟析构函数将确保即使您在给定示例中Bar基指针,也将调用右Bar。你可能需要dllexport operator delete,因为内联有时会让你感到惊讶。

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

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

为此,您必须相信operator delete中的代码才能调用unique_ptr,或者您已将问题带回来。所以这个按照惯例更加保护,而特定于类的shared_ptr / Bar表达了对该类型的内存分配以某种方式完成的要求。