Ошибка удаления std::vector в 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;
}

[РЕДАКТИРОВАТЬ]

В DLL2

in .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::`vector deleting destructor'() + 0x65 байт C++

Спасибо.

ОБНОВИТЬ:

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

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

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

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

ОБНОВЛЕНИЕ2:

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

Кто-нибудь видит какие-либо проблемы с тем, как создаются мои библиотеки?


person Igor    schedule 02.02.2016    source источник
comment
Что ж, я надеюсь, что вы никогда не сделаете копию Foo, потому что сгенерированный компилятором конструктор копирования выполнит поверхностную копию pimpl, что приведет к двойному удалению.   -  person AndyG    schedule 02.02.2016
comment
Вы не следуете правилу трех. Теперь, помимо этого, я думаю, вы должны показать, как вы используете Foo. Неясно, следуете ли вы рекомендациям по выделению и освобождению памяти с помощью библиотек DLL. В частности, тот, кто выделил Foo, должен освободить Foo. Так ли это?   -  person paddy    schedule 02.02.2016
comment
Это не проблема, но нет смысла устанавливать pimpl в NULL в деструкторе Foo. Объект уходит, поэтому pimpl тоже уходит.   -  person Pete Becker    schedule 02.02.2016
comment
Возможно, вам потребуется опубликовать больше кода. В опубликованном вами коде нет Base, поэтому с ошибками, выходящими из его деструктора, очень трудно помочь.   -  person TBBle    schedule 02.02.2016
comment
Я создал небольшой проект, используя опубликованный код, и не получил сбой. Это означает, что есть что-то еще и, возможно, не в коде, которым вы с нами поделились. Кстати, я рекомендую вам то же самое (сделать небольшой проект на основе вашего поста). Возможно, это поможет вам быстрее определить проблему.   -  person StahlRat    schedule 02.02.2016
comment
@StahlRat, да, я мог бы сделать именно это. Я подумал, что, может быть, есть что-то очевидное, что я упускаю...   -  person Igor    schedule 02.02.2016
comment
@TBBle, когда я не работаю, я опубликую больше кода. Надеюсь, здесь кто-нибудь останется.   -  person Igor    schedule 02.02.2016
comment
@PeteBecker, я пытался проверить, не является ли он двойным бесплатным, и поэтому, чтобы предотвратить это, установите для него значение NULL.   -  person Igor    schedule 02.02.2016
comment
@падди, да, это так. У меня есть тестовый код, который выделяет указатель, а затем сразу же удаляет его.   -  person Igor    schedule 02.02.2016
comment
@AndyG, нет, он передается по указателю/ссылке.   -  person Igor    schedule 02.02.2016
comment
@Igor: Я пытался проверить, является ли это двойным бесплатным, и поэтому, чтобы предотвратить это, установите его в NULL .. Это не помешает. Вы бы установили NULL на копию. Попробуйте отключить (=delete) или реализовать конструктор копирования и посмотреть, что произойдет.   -  person AndyG    schedule 02.02.2016
comment
@TBBle, я разместил дополнительный код.   -  person Igor    schedule 03.02.2016
comment
@AndyG, добавлен дополнительный код с контекстом.   -  person Igor    schedule 03.02.2016
comment
@Igor: Бар страдает от той же проблемы с поверхностным копированием, что и Фу   -  person AndyG    schedule 03.02.2016
comment
Сбой происходит после того, как я выхожу из деструктора Base. Вы имеете в виду здесь деструктор Bar? В опубликованном коде по-прежнему нет class Base.   -  person TBBle    schedule 04.02.2016
comment
Из обновления: dll1.dll!Foo::`vector deleting destructor' не является деструктором std::vector. Это «окончательный код очистки» Foo, поэтому код, который запускается сразу после завершения написанного кода ~Foo. Это внутренняя деталь объектной модели Visual C++. .   -  person TBBle    schedule 06.02.2016
comment
Кстати, я не верю, что это связано с pImpl. Вы должны обнаружить, что если вы замените Impl* pImpl простым bool, произойдет то же самое.   -  person TBBle    schedule 06.02.2016
comment
Здесь уже нет однозначного вопроса. В заголовке говорится, что вы получаете сообщение об ошибке во время удаления, но вопрос (перед всеми изменениями) заключается в том, следует ли вам реализовать конструктор копирования. Есть много красных сельдей и несвязанных ошибок, отвлекающих от проблем. Этот вопрос слишком несфокусирован, чтобы помочь будущим посетителям. Я предлагаю начать новый, очень конкретный вопрос.   -  person Adrian McCarthy    schedule 06.02.2016


Ответы (2)


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

По сути, вы динамически выделяете экземпляр Impl в конструкторе Foo с помощью new, у вас есть виртуальный деструктор для Foo, освобождающий управляемый ресурс (pimpl) с помощью delete, но ваш класс Foo уязвим для копий.
На самом деле компилятор сгенерировал конструктор копирования, а операторы присваивания копий выполняют копии по элементам, которые в основном являются поверхностными копиями члена данных указателя pimpl: это источник "утечки" .

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

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

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

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


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

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

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

person Mr.C64    schedule 02.02.2016
comment
Спасибо. Я попробую, когда вернусь домой. Выложу сюда, если что. Кроме того, они должны быть частными? - person Igor; 02.02.2016
comment
@Igor: Не уверен, что понимаю твой вопрос. В любом случае, как уже было сказано, объявление конструктора приватной копии и оператора присваивания отключает вызов автоматически сгенерированных компилятором поддельных неглубоких копий. - person Mr.C64; 02.02.2016
comment
Я добавил заглушку конструктора копирования и заглушку operator=, возвращающую *this как частные члены, и она все еще дает сбой. Может быть, они должны быть публичными с реальной реализацией? - person Igor; 03.02.2016
comment
пожалуйста, смотрите обновление. Вызов new, а затем удаление в той же функции по-прежнему приводит к сбою. - person Igor; 05.02.2016
comment
Вы должны предоставить простую компилируемую реплику для сбоя, чтобы сделать его правильным вопросом StackOverflow. В нынешнем виде мне кажется, что это больше похоже на запрос на консультацию, которую я не могу выполнить без надлежащего бюджета. Другие, у кого пропускная способность больше, чем у меня, могут помочь. Просто примечание: в вашем последнем обновлении вопроса у вас есть тело MyFunc с ...delete param; return param;. Это ферма ошибок, поскольку вы возвращаете висячий (удаленный) указатель. Вам может понадобиться кто-то, кто сможет обучить вас некоторым основным вопросам C++ и DLL. Более того, ошибка может быть в коде, который вы не показали. Удачи. - person Mr.C64; 05.02.2016
comment
Да, я могу это сделать. На какой файлообменник я могу загрузить проект? Потому что это также могут быть некоторые настройки проекта... Я компилирую и запускаю его под Windows 8.1 + MSVC 2010. Код в обновлении указывает на то, что даже если я удалю указатель сразу после выделения памяти в той же функции, он все равно вылетает. . И да, указатель болтается, но для верности я прогнал его через отладчик. - person Igor; 05.02.2016

Проблема в том, что вы выделили Bar в DLL3, которая включает содержащийся экземпляр 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'(), который вы можете увидеть в псевдокоде по адресу Несоответствие скаляра и вектора создать и удалить.

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

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

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

Из статьи поддержки MS 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, так как вставка может иногда удивить вас здесь.

Дополнительные сведения см. в статье поддержки MS 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 выражают требование, чтобы выделение памяти для этого типа выполнялось определенным образом.

person TBBle    schedule 04.02.2016
comment
пожалуйста, смотрите обновление. Даже если я выделяю память, а затем вызываю удаление для указателя, все равно происходит сбой. - person Igor; 05.02.2016
comment
Если трассировка стека не изменилась, значит, вы по-прежнему выделяете память в DLL3 и удаляете в DLL1. Память выделяется там, где вызывается new, и удаляется там, где определено ~Foo. Неважно, куда вы переместите delete. - person TBBle; 06.02.2016
comment
почему в функции удаления () есть дополнительный параметр размера? И IIUC, с этим дополнением все управление памятью будет происходить внутри DLL1/DLL2, верно? - person Igor; 06.02.2016
comment
Спасибо. После добавления конструктора/деструктора в Foo он работал и не зависал. - person Igor; 06.02.2016
comment
Я говорил слишком быстро, это сработало, когда я ставил новые и удалял в одной функции. Но после расщепления опять завис. Похоже, указатель плохой. Думаю, мне следует просто установить класс Foo внутри основного приложения и попробовать его таким образом. - person Igor; 07.02.2016
comment
Более того, похоже, что указатель в порядке, пока библиотека не загружена. Когда библиотека выгружается, указатель становится плохим. - person Igor; 07.02.2016
comment
Верно. Если вы используете специфичные для класса operator new и operator delete, то память выделяется внутри DLL, где они реализованы. И поэтому, если вы выгрузите DLL, эта память станет недействительной, и ваша программа умрет. С другой стороны, если ваша память была выделена в основном приложении, и вы выгружаете DLL, тогда указатель действителен, но все вызовы методов будут с недопустимыми указателями, и ваша программа умрет. Так что не выгружайте DLL, пока у вас еще есть объекты, созданные этой DLL. - person TBBle; 07.02.2016
comment
Специфичный для класса operator delete может принимать второй параметр size_t, который представляет собой то же значение, которое было передано в специфический для класса operator new в первую очередь. Однако в этом нет необходимости, если вы переходите прямо к ::operator delete, поэтому вы можете оставить параметр отключенным, как в примерах на связанных страницах. Это полезно, если вы делаете что-то вроде реализации пула на основе размера и не хотите отслеживать, из какого пула пришел указатель, или искать указатель в каждом пуле. - person TBBle; 07.02.2016