Не вызывается виртуальный деструктор производного класса

Рейтинг: -3Ответов: 2Опубликовано: 12.07.2023

Есть решение, где динамическая библиотека (SharedLibrary) предоставляет интерфейс (абстрактный класс), а другой проект - его реализацию (производный класс). Концепция минимального примера:

концептуальная структура решения

Класс Website выделяет 64 килобайта для буфера, куда помещает URL адрес. Далее, не покидая конструктор, симулируется вызов исключения. Ожидается, что деструктор Website будет вызван, однако вызывается только деструктор базового класса InternetResource:

Website::ctor
InternetResource::dtor
main::catch 'Oops!'

Следовательно, выделенная память не освобождается:

отладка утечки памяти после ожидаемого удаления экземпляра Website

Решение

Исходный код

//
// Application/Main.cpp
//

#include <iostream>

#include "Website.hpp"

int main()
{
    try
    {
        __debugbreak(); // for heap snapshot (1)
        Website site("https://stackoverflow.com"); // throws
        return 1; // unattainable
    }
    catch (const std::exception& e)
    {
        std::cout
            << "main::catch '"
            << e.what()
            << "'"
            << std::endl;
    }
    __debugbreak(); // for heap snapshot (2)
    return 0;
}

//
// Application/Website.hpp
//

#pragma once

#include <SharedLibrary/InternetResource.hpp>

class Website : public InternetResource
{
public:
    Website(const char* url);

    virtual ~Website();

    void* Buffer = nullptr;
};

//
// Application/Website.cpp
//

#include "Website.hpp"

#include <iostream>
#include <memory>

Website::Website(const char* url)
{
    std::cout << "Website::ctor" << std::endl;
    this->Buffer = malloc(1 << 16); // 64 kB
    memcpy_s(this->Buffer, 1 << 16, url, strlen(url));
    throw std::exception("Oops!");
}

Website::~Website()
{
    std::cout << "Website::dtor" << std::endl;
    free(this->Buffer);
    this->Buffer = nullptr;
}

//
// SharedLibrary/Dll.hpp
//

#pragma once

#ifdef SHARED_LIBRARY_EXPORTS
#   define SHARED_LIBRARY_API __declspec(dllexport)
#else
#   define SHARED_LIBRARY_API __declspec(dllimport)
#endif

//
// SharedLibrary/InternetResource.hpp
//

#pragma once

#include "Dll.hpp"

class SHARED_LIBRARY_API InternetResource
{
public:
    virtual ~InternetResource();
};

//
// SharedLibrary/InternetResource.cpp
//

#include "InternetResource.hpp"

#include <iostream>

InternetResource::~InternetResource()
{
    std::cout << "InternetResource::dtor" << std::endl;
}
--
-- Premake5.lua
--
workspace "WhatTheDtor"
    location("Build/")

    configurations { "Debug" }
    platforms { "x86" }

function cpp_commons()
    language "C++"
    cppdialect "C++17"
    
    defines { "DEBUG" }
    symbols "On"

    runtime "Debug"
    staticruntime "Off" -- /MDd

    exceptionhandling ("Default") -- /EHsc
end

project "SharedLibrary"
    kind "SharedLib"
    cpp_commons()
    
    files { "./SharedLibrary/**.cpp", "./SharedLibrary/**.hpp" }
    
    defines { "SHARED_LIBRARY_EXPORTS" }

project "Application"
    kind "ConsoleApp"
    cpp_commons()
    
    files { "./Application/**.cpp", "./Application/**.hpp" }

    includedirs { "." }
    dependson { "SharedLibrary" }
    links { "SharedLibrary" }

Воспроизведение

  1. Соберите файлы по Application и SharedLibrary.
  2. Используйте premake5 для создания файлов решения.
premake5 vs2022
  1. Откройте './Build/WhatTheDtor.sln'.
  2. Соберите и начните отладку с профилированием кучи.
  3. Сделайте два снимка кучи на вшитых точках останова.

Вот и все. Здесь вы можете увидеть 64 килобайта неосвобожденной памяти.

Ответы

▲ 1Принят

Ниже пример как можно сделать void * буфер, который будет освобождён нормально, если конструктор бросит исключение. void * спрятан в unique_ptr.

Общая идея - поля сложных объектов должны сами за собой ухаживать. Тогда компилятор создаст корректный код обрабатывающий исключения в конструкторе.

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>

using namespace std;

void freeVoid(void* data) {
    cout << "freeVoid\n";
    free(data);
}

class Website {
public:
    Website(const char* url) : buffer(malloc(1 << 16), freeVoid) {
        cout << "Website::ctor\n";
        memcpy(buffer.get(), url, strlen(url) + 1);
        // cout << static_cast<char *>(buffer.get()) << '\n';
        throw exception();
    }

    virtual ~Website() {
        cout << "Website::dtor\n";
    }

    unique_ptr<void, decltype(freeVoid) *> buffer;
};

int main() {
    try {
        Website w("site");
    } catch (exception &e) {
        cout << "caught\n";
    }
}
$ g++ website.cpp && ./a.out 
Website::ctor
freeVoid
caught
▲ -3

В документации сказано:

… the process of unwinding the stack begins. This involves the destruction of all automatic objects that were fully constructed—but not yet destructed—between the beginning of the try block that is associated with the catch handler and the throw site of the exception.

MSDN: Exceptions and Stack Unwinding in C++ [п.5]

Иными словами, если исключение возникает в конструкторе, то нет никакого иного способа освободить ресурсы, кроме как поместить весь конструктор в try-catch. Например:

Website::Website(const char* url)
{
    try
    {
        std::cout << "Website::ctor" << std::endl;
        this->Buffer = malloc(1 << 16); // 64 kB
        memcpy_s(this->Buffer, 1 << 16, url, strlen(url));
        throw std::exception("Oops!");
    }
    catch (...)
    {
        std::cout << "**forcing dtor from ctor**" << std::endl;
        this->~Website();
        throw;
    }
}