Error deleting std::vector in a DLL using the PIMPL idiom

前端 未结 2 435
独厮守ぢ
独厮守ぢ 2021-01-29 06:51

I have the following code:

In DLL1:

in .h file:

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

class __declspec(dlle         


        
2条回答
  •  悲哀的现实
    2021-01-29 07:15

    The problem is that you've allocated Bar in DLL3, which includes the contained instance of Foo. However, you deleted it in the main application via the Foo*, which has done the deletion in DLL1 (as seen in your stack trace).

    The debug heap checker has caught you allocating memory in one module and freeing it in another module.


    Detailed explanation of the issue:

    Calling new Foo(args...) does roughly the following:

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

    In the MS Visual Studio C++ object model, this is inlined at the call of new Foo, so happens where you call the new statement.

    Calling delete pFoo does roughly the following:

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

    In the MS Visual Studio C++ object model, both of these operations are compiled into ~Foo, in the Foo::`vector deleting destructor'(), which you can see in psuedocode at Mismatching scalar and vector new and delete.

    So unless you change this behaviour, ::operator new will be called at the site of new Foo, and ::operator delete will be called at the site of the closing brace of ~Foo.

    I haven't detailed virtual or vector behaviours here, but they don't carry any further surprises beyond the above.

    Class-specific overloads of operator new and operator delete are used instead of ::operator new and ::operator delete in the above, if they exist, which lets you control where ::operator new and ::operator delete are called, or even to call something else entirely (e.g. a pool allocator). That's how you explicitly solve this issue.

    I understood from MS Support Article 122675 that MSVC++ 5 and later is supposed to not include the ::operator delete call in the destructor of dllexport/dllimport classes with a virtual destructor, but I never managed to trigger that behaviour, and have found it much more reliable to be explicit about where my memory is allocated/deallocated for DLL-exported classes.


    To fix this, give Foo class-specific overloads of operator new and operator delete, e.g.,

    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();
    };
    

    Don't put the implementations in the header, or it'll be inlined, which defeats the point of the exercise.

    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);
    }
    

    Doing this only for Foo will cause both Foo and Bar to be allocated and destroyed in the context of DLL1.

    If you'd rather Bar be allocated and deleted in the context of DLL2, then you can give it one as well. The virtual destructor will ensure that the right operator delete will be called even if you delete the base pointer as in your given example. You might have to dllexport Bar though, as the inliner can sometimes surprise you here.

    See MS Support Article 122675 for some more details, although you've actually bounced off the opposite problem than the one they describe there.


    Another option: make Foo::Foo protected, and Bar::Bar private, and expose static factory functions for them from your DLL interface. Then the ::operator new call is in the factory function rather than the caller's code, which will put it in the same DLL as the ::operator delete call, and you get the same effect as providing class-specific operator new and operator delete, as well as all the other advantages and disadvantages of factory functions (which are a great improvement once you stop passing raw pointers around and start using unique_ptr or shared_ptr depending on your requirements).

    To do this, you have to trust the code in Bar to not call new Foo, or you've brought the problem back. So this one is more protection by convention, while class-specific operator new/operator delete expresses the requirement that memory allocation for that type be done in a certain way.

提交回复
热议问题