2016-02-02 6 views
-2

У меня есть следующий код:Ошибки при удалении зОго :: вектора в DLL с помощью Pimpl идиомы

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

[EDIT]

В 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 также иметь экземпляр-конструктор и оператор присваивания? [/ EDIT]

Обратите внимание, что MyClass не экспортируется и не имеет деструктора.

Как правило, вы пишете код?

Проблема в том, что я столкнулся с Windows (8.1 + MSVC 2010, если это имеет значение). Я могу отправить больше кода, если это необходимо, но на данный момент просто хочу убедиться, что я не делаю что-то явно неправильно.

Катастрофа происходит после того, как я выхожу из базы деструктора и трассировки стека говорит:

ntdll.dll 770873a6() [Фреймы ниже могут быть неправильными и/или отсутствует, никакие символы не загружены для ntdll.dll] ntdll.dll! 7704164f()
ntdll.dll! 77010f01() KernelBase.dll! 754a2844()
dll1.dll! _CrtIsValidHeapPointer (сопзЬ пустота * 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! Оператор удаления (аннулируются * pUserData) Строка 54 + 0x10 байт C++ dll1.dll! Foo :: `вектор удаления деструктор '() + 0x65 байт C++

Спасибо.

UPDATE:

Даже если я ставлю следующий код в

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

Программа по-прежнему сбой в операции удаления паров в том же месте.

Похоже, деструктор std :: vector вызывается позже, после вызова деструктора Foo. Так оно и должно быть?

UPDATE2:

После тщательного запуска этого под отладчиком, я вижу, что авария происходит внутри «аннулируется оператор удаления (пустота * 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 "/ Б/analyze-/errorReport: очереди

Библиотекарь /OUT:" C: \ Users \ Игорь \ OneDrive \ Docu Ментов \ 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

Надеюсь, вы никогда не сделаете копию 'Foo', потому что созданный компилятором экземпляр будет выполнять мелкую копию' pimpl', что приведет к двойному удалению. – AndyG

+1

Вы не следуете правилам Трех. Теперь, с учетом этого, я думаю, вы должны показать, как вы используете 'Foo'. Неясно, следуете ли вы указаниям по распределению памяти и освобождению библиотек DLL. В частности, тот, кто выделил 'Foo', должен освободить' Foo'. Это так? – paddy

+0

Это не проблема, но нет смысла устанавливать 'pimpl' в' NULL' в деструкторе 'Foo'. Объект уходит, поэтому 'pimpl' тоже уходит. –

ответ

0

Без дополнительного кода для анализа важная ошибка, которую я вижу в вашем опубликованном коде, заключается в том, что ваш класс Foo является менеджером ресурсов, нарушающим так называемый Rule of Three.

В принципе, вы динамически выделять Impl экземпляр в Foo конструктора с использованием new, у вас есть виртуальный деструктор Foo рилизинг управляемый ресурс (pimpl) с delete, но ваш Foo класс уязвимы для копий.
В самом деле, компилятор конструктор копирования и копирование операторы присваивания выполняет член-накрест копию, которые в основном мелкие ксерокопии из элемента данных pimpl указателя: это источник «leaktrocities».

Вы можете объявить приватный конструктор копировать и копировать задание на Foo, чтобы отключить сгенерированного компилятором операции копирования членов-накрест:

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

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

Примечание: 11 в C++ =delete синтаксис отключенные копии недоступны в MSVC 2010, поэтому я включил его в комментарии.


Не напрямую связан с вашей проблемой, но, возможно, стоит отметить:

  1. В вашей Foo::Impl структуры, так как член m_vec данные уже public, я не вижу никакой непосредственной причины, чтобы обеспечить наносящий элемент аксессора функция как GetVector().

  2. Начиная с C++ 11, рекомендуется использовать nullptr вместо NULL в вашем коде.

+0

thx. Я попробую, когда вернусь домой. Будут публиковать здесь, если угодно. Кроме того, они должны быть частными? – Igor

+0

@Igor: Не уверен, что я понимаю ваш вопрос. Во всяком случае, как уже было написано, объявление частного экземпляра-конструктора и оператора присваивания запрещает вызывать ложные мелкие копии с автоматическим компилятором. –

+0

еще код добавлен с дополнительным вопросом. Спасибо. – Igor

0

Проблема заключается в том, что вы выделили Bar в DLL3, который включает в себя Содержится экземпляр Foo. Однако вы удалили его в основном приложении через Foo*, который сделал удаление в DLL1 (как видно из вашей трассировки стека).

Отказоустойчивая система кучи поймала выделение памяти в одном модуле и освобождение ее в другом модуле.


Подробное описание вопроса:

Calling 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'(), которые вы можете увидеть в psuedocode в Mismatching scalar and vector new and delete.

Так что если вы не изменить это поведение, ::operator new будет называться на сайте new Foo и ::operator delete будет называться на месте закрывающей фигурной скобки ~Foo.

У меня нет подробного описания виртуального или векторного поведения здесь, но они не имеют никаких дальнейших сюрпризов за пределами вышеизложенного.

класса специфических перегруженные operator new и operator delete используются вместо ::operator new и ::operator delete в выше, если они существуют, что позволяет контролировать, где ::operator new и ::operator delete называются, или даже назвать что-то совсем другое (например, бассейн распределителем). Вот как вы решительно решаете эту проблему.

я понял из MS Support Article 122675 что MSVC++ 5, а затем, как предполагается, не включать ::operator delete вызов деструктора dllexport/dllimport классов с виртуальным деструктора, но мне никогда не удавалось вызвать такое поведение, и нашел его более надежным чтобы быть явным, где моя память распределена/освобождена для DLL-экспортированных классов.


Чтобы это исправить, дать Foo классовые перегруженные operator new и operator 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 будет вызывать как Foo и Bar быть выделены и уничтожены в контексте Dll1.

Если вы хотите выделить Bar в контексте DLL2, вы также можете указать его. Виртуальный деструктор гарантирует, что будет вызываться правый operator delete, даже если вы указали базовый указатель, как в приведенном примере, delete. Возможно, вам понадобится dllexport Bar, хотя, как видите, inliner может удивить вас здесь.

Для получения более подробной информации см. MS Support Article 122675, хотя вы действительно отбросили противоположную проблему, чем та, которую они там описывают.


Другой вариант: сделать Foo::Foo защищен, и Bar::Bar частные, и подвергать статические фабричные функции для них из своего интерфейса DLL. Затем вызов ::operator new находится в заводской функции, а не в коде вызывающего абонента, который поместит его в ту же DLL, что и вызов ::operator delete, и вы получите тот же эффект, что и предоставление класса operator new и operator delete, а также все другие преимущества и недостатки заводских функций (которые являются отличным улучшением после того, как вы прекратите передавать исходные указатели и начнете использовать unique_ptr или shared_ptr в зависимости от ваших требований).

Для этого вам нужно доверять код в Bar, чтобы не звонить new Foo, или вы вернули проблему. Таким образом, это больше защищает по соглашению, в то время как для класса operator new/operator delete выражается требование, чтобы распределение памяти для этого типа выполнялось определенным образом.

+0

см. Обновление. Даже если я выделяю память, а затем вызываю delete на указателе, он все равно сбой. – Igor

+0

Если трассировка стека не изменилась, вы все еще выделяете в DLL3 и удаляете в DLL1. Память выделяется там, где вызывается 'new', и удаляется там, где определен' ~ Foo'. Неважно, где вы перемещаете 'delete'. – TBBle

+0

Почему в функции delete() есть дополнительный параметр «размер»? И IIUC, с этим дополнением все управление памятью произойдет внутри DLL1/DLL2, правильно? – Igor