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

Другие вопросы по тегам