C++ деструктор не вызывается, в зависимости от порядка компоновки
Я столкнулся с этой проблемой в своем приложении после проверки его на утечки памяти и обнаружил, что некоторые из моих классов вообще не уничтожаются.
Приведенный ниже код разделен на 3 файла, предполагается, что он реализует шаблон, называемый pimpl. Ожидаемый сценарий состоит в том, чтобы Cimpl
конструктор и деструктор печатают свои сообщения. Однако это не то, что я получаю с g++. В моем приложении вызывался только конструктор.
classes.h:
#include <memory>
class Cimpl;
class Cpimpl {
std::auto_ptr<Cimpl> impl;
public:
Cpimpl();
};
classes.cpp:
#include "classes.h"
#include <stdio.h>
class Cimpl {
public:
Cimpl() {
printf("Cimpl::Cimpl()\n");
}
~Cimpl() {
printf("Cimpl::~Cimpl()\n");
}
};
Cpimpl::Cpimpl() {
this->impl.reset(new Cimpl);
}
main.cpp:
#include "classes.h"
int main() {
Cpimpl c;
return 0;
}
Вот что я смог открыть дальше:
g++ -Wall -c main.cpp
g++ -Wall -c classes.cpp
g++ -Wall main.o classes.o -o app_bug
g++ -Wall classes.o main.o -o app_ok
Похоже, деструктор вызывается в одном из двух возможных случаев, и это зависит от порядка связывания. С app_ok я смог получить правильный сценарий, в то время как app_bug вел себя точно так же, как мое приложение.
Есть ли какая-то мудрость, которой мне не хватает в этой ситуации? Спасибо за любое предложение заранее!
4 ответа
Цель идиомы pimpl состоит в том, чтобы не указывать определение класса реализации в заголовочном файле. Но все стандартные интеллектуальные указатели требуют, чтобы определение их параметра шаблона было видимым в точке объявления для правильной работы.
Это означает, что это один из редких случаев, когда вы действительно хотите использовать new
, delete
и голый указатель. (Если я ошибаюсь по этому поводу и есть стандартный умный указатель, который можно использовать для pimpl, кто-нибудь, пожалуйста, дайте мне знать.)
classes.h
struct Cimpl;
struct Cpimpl
{
Cpimpl();
~Cpimpl();
// other public methods here
private:
Cimpl *ptr;
// Cpimpl must be uncopyable or else make these copy the Cimpl
Cpimpl(const Cpimpl&);
Cpimpl& operator=(const Cpimpl&);
};
classes.cpp
#include <stdio.h>
struct Cimpl
{
Cimpl()
{
puts("Cimpl::Cimpl()");
}
~Cimpl()
{
puts("Cimpl::~Cimpl()");
}
// etc
};
Cpimpl::Cpimpl() : ptr(new Cimpl) {}
Cpimpl::~Cpimpl() { delete ptr; }
// etc
Проблема заключается в том, что в момент определения auto_ptr<Cimpl>
объект, Cimpl
является неполным типом, то есть компилятор видел только предварительное объявление Cimpl
, Это нормально, но, поскольку он в конечном итоге удаляет объект, на который он содержит указатель, вы должны выполнить это требование из [expr.delete]/5:
Если удаляемый объект имеет неполный тип класса в точке удаления, а полный класс имеет нетривиальный деструктор или функцию освобождения, поведение не определено.
Таким образом, этот код имеет неопределенное поведение, и все ставки отключены.
Кодекс нарушает Правило Единого Определения. Там есть определение класса Cimpl
в classes.h, и другое определение класса Cimpl
в файле classes.cpp. Результатом является неопределенное поведение. Можно иметь более одного определения класса, но они должны быть одинаковыми.
Отредактировано для ясности, оригинал сохранен ниже.
Этот код имеет неопределенное поведение, потому что в контексте main.cpp
неявный Cpimpl::~Cpimpl
деструктор имеет только предварительное объявление Cimpl
, но auto_ptr
(или любая другая форма выполнения delete
) нуждается в полном определении, чтобы юридически очистить Cimpl
, Учитывая, что это неопределенное поведение, дальнейшее объяснение ваших наблюдений не требуется.
Оригинальный ответ:
Я подозреваю, что здесь происходит то, что неявный деструктор Cpimpl
генерируется в контексте classes.h
и не имея доступа к полному определению Cimpl
, Тогда когда auto_ptr
пытается сделать свое дело и очистить содержащийся в нем указатель, удаляет неполный класс, поведение которого не определено. Учитывая, что он не определен, нам не нужно идти дальше, чтобы объяснить, что для него вполне приемлемо работать по-разному в зависимости от порядка ссылок.
Я подозреваю, что явный деструктор для Cpimpl
с определением в исходном файле решит вашу проблему.
РЕДАКТИРОВАТЬ: На самом деле теперь, когда я смотрю на это снова, я считаю, что ваша программа нарушает одно правило определения в его нынешнем виде. В main.cpp
он видит неявный деструктор, который не знает, как вызвать деструктор Cimpl
(потому что он имеет только предварительную декларацию). В classes.cpp
неявный деструктор имеет доступ к Cimpl
и как его называть деструктором.