Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в C++.

Использование классов в C++

Классы в Plugify предоставляют более удобный, типобезопасный и совместимый с RAII способ работы со сложными объектами, экспортируемыми плагинами. Вместо ручного управления сырыми указателями и вызова функций конструктора/деструктора, вы можете использовать сгенерированные обёртки классов C++, которые автоматически управляют ресурсами.

Зачем использовать классы?

Когда плагин экспортирует функции для создания и уничтожения объектов (например, Kv1Create и Kv1Destroy), вы можете вызывать эти функции напрямую:

// Ручной подход - подвержен ошибкам
void* kv = example_plugin::Kv1Create("Config");
example_plugin::Kv1SetName(kv, "ServerConfig");
auto name = example_plugin::Kv1GetName(kv);
example_plugin::Kv1Destroy(kv);  // Легко забыть!

Однако у этого подхода есть несколько проблем:

  1. Утечки ресурсов: Если вы забудете вызвать Kv1Destroy(), ресурс утечёт
  2. Небезопасность при исключениях: Если возникнет исключение, деструктор не будет вызван
  3. Отсутствие типобезопасности: Сырые указатели void* не обеспечивают проверку типов во время компиляции
  4. Многословность: Вы должны вручную передавать дескриптор каждому вызову функции
  5. Подверженность ошибкам: Легко использовать дескриптор после его уничтожения

Классы решают все эти проблемы, используя RAII C++ (Resource Acquisition Is Initialization):

// Подход RAII - автоматический, безопасный, устойчивый к исключениям
example_plugin::KeyValues kv("Config");
kv.SetName("ServerConfig");
auto name = kv.GetName();
// Автоматически уничтожается, когда kv выходит из области видимости!

Как работают классы

Когда вы запускаете plugify-gen -lang cpp -classes, генератор создаёт класс C++ для каждой записи в разделе classes манифеста.

Определение манифеста

{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    { "name": "Kv1Create", "funcName": "Kv1Create", "paramTypes": [{ "name": "setName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1Destroy", "funcName": "Kv1Destroy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1GetName", "funcName": "Kv1GetName", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "string" } },
    { "name": "Kv1SetName", "funcName": "Kv1SetName", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "name", "type": "string" }], "retType": { "type": "void" } },
    { "name": "Kv1FindKey", "funcName": "Kv1FindKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetFirstSubKey", "funcName": "Kv1GetFirstSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1AddSubKey", "funcName": "Kv1AddSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "subKey", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1MakeCopy", "funcName": "Kv1MakeCopy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetString", "funcName": "Kv1GetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "defaultValue", "type": "string" }], "retType": { "type": "string" } },
    { "name": "Kv1SetString", "funcName": "Kv1SetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "value", "type": "string" }], "retType": { "type": "void" } }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        { "name": "GetName", "method": "Kv1GetName", "bindSelf": true },
        { "name": "SetName", "method": "Kv1SetName", "bindSelf": true },
        { "name": "FindKey", "method": "Kv1FindKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "GetFirstSubKey", "method": "Kv1GetFirstSubKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "AddSubKey", "method": "Kv1AddSubKey", "bindSelf": true, "paramAliases": [{ "name": "KeyValues", "owner": true }] },
        { "name": "MakeCopy", "method": "Kv1MakeCopy", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": true } },
        { "name": "GetString", "method": "Kv1GetString", "bindSelf": true },
        { "name": "SetString", "method": "Kv1SetString", "bindSelf": true }
      ]
    }
  ]
}

Ключевые особенности манифеста для классов

  1. Класс (classes[]): Каждая запись описывает одну обёртку: её имя name, тип хендла handleType (здесь ptr64) и значение invalidValue, означающее пустой хендл ("0")
  2. Конструкторы (constructors): Имена методов, возвращающих новый хендл; каждый становится конструктором класса
  3. Деструктор (destructor): Имя метода, освобождающего хендл; вызывается автоматически, когда объект выходит из области видимости
  4. Привязки (bindings): Методы, доступные у класса. При "bindSelf": true хендл автоматически передаётся первым аргументом, и этот параметр исчезает из сгенерированной сигнатуры
  5. Владение (owner): Задаётся в paramAliases / retAlias привязки; определяет, берёт ли класс владение параметром или возвращаемым значением

Сгенерированный класс

Из приведённого выше манифеста генератор создаёт класс KeyValues:

namespace example_plugin {

  enum class Ownership : bool { Borrowed, Owned };

  /**
   * @brief Обёртка RAII для дескриптора KeyValues.
   */
  class KeyValues final {
  public:
    KeyValues() = default;

    /**
     * @brief Создаёт новый экземпляр KeyValues
     * @param setName (string): Имя, присваиваемое этому экземпляру KeyValues
     */
    explicit KeyValues(const plg::string& setName)
      : KeyValues(Kv1Create(setName), Ownership::Owned) {}

    ~KeyValues() {
      destroy();
    }

    // Запрет копирования (вызвал бы двойное освобождение)
    KeyValues(const KeyValues&) = delete;
    KeyValues& operator=(const KeyValues&) = delete;

    // Разрешение перемещения (передача владения)
    KeyValues(KeyValues&& other) noexcept
      : _handle(other._handle)
      , _ownership(other._ownership) {
      other.nullify();
    }

    KeyValues& operator=(KeyValues&& other) noexcept {
      if (this != &other) {
        destroy();
        _handle = other._handle;
        _ownership = other._ownership;
        other.nullify();
      }
      return *this;
    }

    // Конструирование из сырого дескриптора с контролем владения
    KeyValues(void* handle, Ownership ownership = Ownership::Borrowed)
      : _handle(handle), _ownership(ownership) {}

    // Получить сырой дескриптор без передачи владения
    [[nodiscard]] auto get() const noexcept { return _handle; }

    // Передать владение вызывающей стороне (освобождает внутренний дескриптор)
    [[nodiscard]] auto release() noexcept {
      auto handle = _handle;
      nullify();
      return handle;
    }

    // Уничтожить ресурс и очистить дескриптор
    void reset() noexcept {
      destroy();
      nullify();
    }

    // Обменять с другим экземпляром
    void swap(KeyValues& other) noexcept {
      using std::swap;
      swap(_handle, other._handle);
      swap(_ownership, other._ownership);
    }

    friend void swap(KeyValues& lhs, KeyValues& rhs) noexcept {
      lhs.swap(rhs);
    }

    // Проверить, является ли дескриптор валидным
    explicit operator bool() const noexcept {
      return _handle != nullptr;
    }

    // Операторы сравнения
    [[nodiscard]] auto operator<=>(const KeyValues& other) const noexcept {
      return _handle <=> other._handle;
    }

    [[nodiscard]] bool operator==(const KeyValues& other) const noexcept {
      return _handle == other._handle;
    }

    // Привязанные методы (автоматически проверяют валидность дескриптора)
    plg::string GetName() {
      if (_handle == nullptr)
        throw std::runtime_error("KeyValues: empty handle");
      return Kv1GetName(_handle);
    }

    void SetName(const plg::string& name) {
      if (_handle == nullptr)
        throw std::runtime_error("KeyValues: empty handle");
      Kv1SetName(_handle, name);
    }

    KeyValues FindKey(const plg::string& keyName) {
      if (_handle == nullptr)
        throw std::runtime_error("KeyValues: empty handle");
      // Заимствованное владение - вызывающая сторона не владеет возвращаемым объектом
      return KeyValues(Kv1FindKey(_handle, keyName), Ownership::Borrowed);
    }

    void AddSubKey(KeyValues&& subKey) {
      if (_handle == nullptr)
        throw std::runtime_error("KeyValues: empty handle");
      // Принимает владение - subKey перемещается и освобождается
      Kv1AddSubKey(_handle, subKey.release());
    }

  private:
    void destroy() const noexcept {
      if (_handle != nullptr && _ownership == Ownership::Owned) {
        Kv1Destroy(_handle);
      }
    }

    void nullify() noexcept {
      _handle = nullptr;
      _ownership = Ownership::Borrowed;
    }

    void* _handle{nullptr};
    Ownership _ownership{Ownership::Borrowed};
  };

} // namespace example_plugin

Утилитарные методы

Каждый сгенерированный класс включает следующие утилитарные методы:

get()

Возвращает сырой дескриптор без передачи владения. Используйте это, когда вам нужно передать дескриптор C-стиль функциям, которые не берут владение:

example_plugin::KeyValues kv("Config");
void* raw_handle = kv.get();
// kv по-прежнему владеет дескриптором - уничтожит его, когда kv выйдет из области видимости

release()

Передаёт владение вызывающей стороне и очищает внутренний дескриптор. Используйте это, когда функция берёт владение объектом:

example_plugin::KeyValues subKey("SubSection");
parent.AddSubKey(std::move(subKey));  // AddSubKey внутренне вызывает release()

// После этого вызова:
// - parent теперь владеет ресурсом subKey
// - объект subKey перемещён и его дескриптор освобождён
// - Когда parent будет уничтожен, он также уничтожит subKey

reset()

Уничтожает управляемый ресурс (если принадлежит) и очищает дескриптор:

example_plugin::KeyValues kv("Config");
kv.reset();  // Немедленно уничтожает ресурс
// kv теперь пуст (handle == nullptr)

swap()

Обменивает состояние с другим экземпляром:

example_plugin::KeyValues kv1("Config1");
example_plugin::KeyValues kv2("Config2");
kv1.swap(kv2);  // Теперь kv1 имеет Config2, kv2 имеет Config1

// Или используя std::swap:
std::swap(kv1, kv2);

operator bool()

Проверяет, является ли дескриптор валидным (не null):

example_plugin::KeyValues kv("Config");
if (kv) {
    // Дескриптор валиден
    kv.SetName("NewName");
}

example_plugin::KeyValues empty;
if (!empty) {
    // Дескриптор равен null
}

Операторы сравнения

Сравнивают экземпляры по значениям их дескрипторов:

example_plugin::KeyValues kv1("Config1");
example_plugin::KeyValues kv2("Config2");

if (kv1 == kv2) {  // Сравнение по дескриптору
    // Один и тот же базовый объект
}

if (kv1 != kv2) {
    // Разные объекты
}

// Оператор spaceship для упорядочивания
if (kv1 < kv2) {
    // Дескриптор kv1 меньше, чем у kv2
}

Управление ресурсами с RAII

Классы C++ используют RAII (Resource Acquisition Is Initialization) для автоматического управления ресурсами. Это самый детерминированный подход среди всех языков, поддерживаемых Plugify.

Автоматическая очистка

Ресурсы автоматически уничтожаются, когда объекты выходят из области видимости:

void processConfig() {
    example_plugin::KeyValues kv("ServerConfig");
    kv.SetName("Production");
    // Ресурс автоматически уничтожается, когда kv выходит из области видимости
} // Деструктор вызывается здесь - Kv1Destroy() вызывается автоматически

Безопасность исключений

RAII гарантирует очистку даже при возникновении исключений:

void riskyOperation() {
    example_plugin::KeyValues kv("Config");

    performDatabaseOperation();  // Может выбросить исключение
    performNetworkOperation();   // Может выбросить исключение
    performFileOperation();      // Может выбросить исключение

    // Если ЛЮБАЯ из вышеперечисленных операций выбросит исключение, деструктор kv всё равно будет вызван
    // Ресурс гарантированно будет очищен!
} // Деструктор вызывается, даже если было выброшено исключение

Это превосходит другие языки:

  • JavaScript: Нет детерминированной очистки, необходимо использовать try-finally
  • Go: Необходимо помнить об использовании defer kv.Close()
  • C#: Необходимо использовать оператор using для детерминированной очистки
  • Python: Необходимо использовать оператор with или вручную вызывать __exit__()
  • C++: Автоматически - специальный синтаксис не требуется!

Семантика перемещения

Классы C++ поддерживают семантику перемещения для эффективной передачи владения:

example_plugin::KeyValues createConfig() {
    example_plugin::KeyValues kv("Config");
    kv.SetName("ServerConfig");
    return kv;  // Перемещение - без копирования, владение передаётся вызывающей стороне
}

void useConfig() {
    example_plugin::KeyValues config = createConfig();  // Конструирование перемещением
    // config теперь владеет ресурсом
} // Ресурс уничтожается здесь

Перемещение эффективно и безопасно:

  • Без копирования: Конструктор перемещения передаёт владение без копирования
  • Без двойного освобождения: Перемещённый объект остаётся в валидном, но пустом состоянии
  • Нулевые накладные расходы: Компиляторы оптимизируют перемещения так же быстро, как передачу указателей

Предотвращение копирования

Классы предотвращают копирование во избежание ошибок двойного освобождения:

example_plugin::KeyValues kv1("Config");
example_plugin::KeyValues kv2 = kv1;  // ОШИБКА: Конструктор копирования удалён

void func(example_plugin::KeyValues kv);  // ОШИБКА: Приведёт к копированию
func(kv1);  // Не скомпилируется

// Используйте перемещение вместо этого:
example_plugin::KeyValues kv2 = std::move(kv1);  // ОК - владение передано
void func2(example_plugin::KeyValues&& kv);  // ОК - принимает rvalue ссылку
func2(std::move(kv1));  // ОК

// Или передавайте по ссылке:
void func3(example_plugin::KeyValues& kv);  // ОК - без передачи владения
func3(kv1);  // ОК

Семантика владения

Владение определяет, кто отвечает за уничтожение объекта. Сгенерированные классы отслеживают владение с помощью перечисления Ownership:

enum class Ownership : bool { Borrowed, Owned };

Принадлежащие ресурсы

Когда вы создаёте объект с помощью конструктора, класс владеет ресурсом и уничтожит его:

example_plugin::KeyValues kv("Config");
// kv владеет ресурсом (_ownership == Ownership::Owned)
// Деструктор вызовет Kv1Destroy(), когда kv выйдет из области видимости

Заимствованные ресурсы

Когда метод возвращает указатель, которым вы не владеете (помечен "owner": false в манифесте), класс создаёт заимствованный экземпляр:

example_plugin::KeyValues parent("Parent");
auto child = parent.FindKey("ChildKey");
// child заимствован (_ownership == Ownership::Borrowed)
// деструктор child НЕ будет вызывать Kv1Destroy()
// parent владеет фактическим дочерним ресурсом

Важно: Заимствованные объекты не должны переживать объект, из которого они были заимствованы:

example_plugin::KeyValues* dangling;
{
    example_plugin::KeyValues parent("Parent");
    auto child = parent.FindKey("Child");
    dangling = &child;  // ОПАСНО!
} // parent уничтожается здесь, забирая child с собой
// dangling теперь указывает на уничтоженный объект - неопределённое поведение!

Передача владения

Некоторые методы берут владение переданными им объектами (помечены "owner": true в манифесте). Эти методы принимают rvalue ссылки и вызывают release():

example_plugin::KeyValues parent("Parent");
example_plugin::KeyValues child("Child");

parent.AddSubKey(std::move(child));
// AddSubKey внутренне вызывает child.release()
// parent теперь владеет ресурсом child
// child теперь пуст (handle == nullptr)

// НЕПРАВИЛЬНО: Не используйте child после перемещения
child.SetName("NewName");  // Выбрасывает исключение: empty handle!

// НЕПРАВИЛЬНО: Не перемещайте из lvalue без std::move
parent.AddSubKey(child);  // Не скомпилируется - требуется rvalue ссылка

Ручное управление владением

Вы можете вручную управлять владением с помощью конструктора KeyValues(void*, Ownership):

void* raw_handle = example_plugin::Kv1Create("Raw");

// Создать владеющую обёртку - уничтожится при выходе из области видимости
example_plugin::KeyValues owned(raw_handle, example_plugin::Ownership::Owned);

// Создать заимствованную обёртку - не уничтожится
example_plugin::KeyValues borrowed(raw_handle, example_plugin::Ownership::Borrowed);

Жизненный цикл плагина и глобальные объекты

Критическое предупреждение о глобальных/статических объектах

Если вы храните экземпляры классов в глобальных переменных, статических переменных или полях класса, вы должны вручную уничтожить их в OnPluginEnd(), чтобы предотвратить сбои при выгрузке плагина:

// ОПАСНО: Глобальный объект
example_plugin::KeyValues g_config;

plg::PluginResult OnPluginStart() override {
    g_config = example_plugin::KeyValues("GlobalConfig");
    // ... использовать config
    return {};
}

plg::PluginResult OnPluginEnd() override {
    // КРИТИЧНО: Уничтожить глобальные объекты ДО выгрузки плагина!
    g_config.reset();  // Или g_config = example_plugin::KeyValues{};
    return {};
}

Почему это необходимо?

Деструкторы C++ для глобальных объектов выполняются при выгрузке вашей библиотеки или завершении процесса, после OnPluginEnd():

  1. Вызывается OnPluginEnd()
  2. Plugify выгружает плагины; плагин, экспортирующий Kv1Destroy(), может быть уже выгружен
  3. Выполняются глобальные деструкторы вашей библиотеки
  4. Глобальный деструктор вызывает Kv1Destroy() через указатель на код, который уже не загружен
  5. Сбой с ошибкой сегментации или нарушением доступа

Решения

Вариант 1: Избегайте глобальных объектов (Рекомендуется)

Используйте локальные переменные с RAII:

plg::PluginResult OnPluginStart() override {
    example_plugin::KeyValues config("Config");
    // Использовать config...
    return {};
} // Автоматически уничтожается до выгрузки плагина

Вариант 2: Ручная очистка в OnPluginEnd()

Если вы должны использовать глобальные переменные, всегда очищайте их:

example_plugin::KeyValues g_config;

plg::PluginResult OnPluginEnd() override {
    g_config.reset();  // Уничтожить СЕЙЧАС, не позже
    return {};
}

Вариант 3: Используйте умные указатели

#include <memory>

std::unique_ptr<example_plugin::KeyValues> g_config;

plg::PluginResult OnPluginStart() override {
    g_config = std::make_unique<example_plugin::KeyValues>("Config");
    return {};
}

plg::PluginResult OnPluginEnd() override {
    g_config.reset();  // Уничтожить и установить в nullptr
    return {};
}

Вариант 4: Используйте std::optional

#include <optional>

std::optional<example_plugin::KeyValues> g_config;

plg::PluginResult OnPluginStart() override {
    g_config.emplace("Config");
    return {};
}

plg::PluginResult OnPluginEnd() override {
    g_config.reset();  // Уничтожить содержащийся объект
    return {};
}

Поля класса

Те же правила применяются к полям класса:

class MyPlugin {
public:
    void start() {
        m_config = example_plugin::KeyValues("Config");
    }

    void stop() {
        // КРИТИЧНО: Уничтожить объекты полей!
        m_config.reset();
    }

private:
    example_plugin::KeyValues m_config;
};

Полный пример

Вот полный пример, демонстрирующий все концепции:

#include <iostream>
#include <plg/plugin.hpp>
#include <plugin_export.h>
#include <example_plugin/example_plugin.hpp>

class ConfigManager {
public:
    void Initialize() {
        // Создать принадлежащий объект
        m_rootConfig = example_plugin::KeyValues("ServerConfig");
        m_rootConfig.SetName("Production");

        // Создать подключи и передать владение
        auto database = example_plugin::KeyValues("Database");
        database.SetString("engine", "PostgreSQL");
        m_rootConfig.AddSubKey(std::move(database));
        // database теперь пуст - владение передано

        auto caching = example_plugin::KeyValues("Caching");
        caching.SetString("engine", "Redis");
        m_rootConfig.AddSubKey(std::move(caching));
    }

    void ProcessConfig() {
        // Find возвращает заимствованную ссылку
        auto dbConfig = m_rootConfig.FindKey("Database");
        if (dbConfig) {
            std::cout << "Database: " << dbConfig.GetString("engine", "") << std::endl;
            // dbConfig заимствован - m_rootConfig по-прежнему владеет им
        }
        // dbConfig уничтожается здесь, но не вызывает Kv1Destroy (заимствован)
    }

    void UpdateConfig() {
        // Создать временный принадлежащий объект
        auto networking = example_plugin::KeyValues("Networking");
        networking.SetString("protocol", "HTTP/2");

        // Передать владение корневому config
        m_rootConfig.AddSubKey(std::move(networking));
        // networking теперь пуст

    } // Временные объекты безопасно уничтожены (если есть)

    void Cleanup() {
        // КРИТИЧНО: Необходимо вызвать перед выгрузкой плагина!
        m_rootConfig.reset();
    }

private:
    example_plugin::KeyValues m_rootConfig;
};

ConfigManager g_manager;

class ExamplePlugin final : public plg::Plugin {
public:
    plg::PluginResult OnPluginStart() final {
        try {
            g_manager.Initialize();
            g_manager.ProcessConfig();
            g_manager.UpdateConfig();
        } catch (const std::exception& e) {
            std::cerr << "Ошибка: " << e.what() << std::endl;
            // Даже если выброшено исключение, объекты очищаются
        }
        return {};
    }

    plg::PluginResult OnPluginEnd() final {
        // КРИТИЧНО: Очистить перед выгрузкой!
        g_manager.Cleanup();
        return {};
    }
} g_examplePlugin;

PLUGIFY_PLUGIN(PLUGIN_API, &g_examplePlugin)

Лучшие практики

  1. Предпочитайте локальные переменные: Используйте RAII с локальными переменными для автоматической очистки
    void process() {
        example_plugin::KeyValues kv("Config");
        // Использовать kv...
    } // Автоматически уничтожается
    
  2. Используйте std::move для передачи владения: Всегда используйте std::move() при передаче владения
    parent.AddSubKey(std::move(child));  // Правильно
    parent.AddSubKey(child);  // Не скомпилируется
    
  3. Проверяйте валидность перед использованием: Используйте operator bool() для проверки валидности дескриптора
    auto child = parent.FindKey("Child");
    if (child) {
        child.SetName("NewName");
    }
    
  4. Очищайте глобальные объекты: Всегда вызывайте reset() в OnPluginEnd() для глобальных/статических объектов
    plg::PluginResult OnPluginEnd() override {
        g_config.reset();
        return {};
    }
    
  5. Не используйте перемещённые объекты: После перемещения объекты пусты
    example_plugin::KeyValues kv("Config");
    auto kv2 = std::move(kv);
    // kv теперь пуст - не используйте его!
    
  6. Используйте const ссылки для доступа только для чтения: Избегайте ненужной передачи владения
    void readConfig(const example_plugin::KeyValues& kv) {
        auto name = kv.GetName();
    }
    
  7. Используйте умные указатели для опционального владения: Когда владение динамическое, используйте std::unique_ptr
    std::unique_ptr<example_plugin::KeyValues> maybeConfig;
    if (needConfig) {
        maybeConfig = std::make_unique<example_plugin::KeyValues>("Config");
    }
    
  8. Используйте безопасность исключений: RAII обеспечивает автоматическую очистку при исключениях
    void riskyOperation() {
        example_plugin::KeyValues kv("Config");
        mightThrow();  // kv всё равно уничтожится, даже если это выбросит исключение
    }
    

Устранение неполадок

Исключение "empty handle"

Проблема: Вы получаете std::runtime_error: KeyValues: empty handle при вызове методов.

Причины:

  • Использование объекта, созданного конструктором по умолчанию, без его инициализации
  • Использование объекта после вызова reset()
  • Использование объекта после перемещения его с помощью std::move()

Решение: Всегда проверяйте валидность перед использованием:

example_plugin::KeyValues kv;  // Пустой
if (!kv) {
    kv = example_plugin::KeyValues("Config");  // Инициализировать
}
kv.SetName("NewName");  // Теперь безопасно

Сбой при выгрузке плагина

Проблема: Приложение падает с ошибкой сегментации при выгрузке плагина.

Причина: Глобальные/статические объекты уничтожаются после выгрузки плагина, пытаясь вызвать функции, которых больше не существует.

Решение: Всегда очищайте глобальные объекты в OnPluginEnd():

plg::PluginResult OnPluginEnd() override {
    g_config.reset();
    g_manager.Cleanup();
    return {};
}

Двойное освобождение или использование после освобождения

Проблема: Сбой или неопределённое поведение при использовании объектов.

Причины:

  • Попытка скопировать объекты (конструктор копирования удалён, поэтому это не скомпилируется)
  • Использование заимствованных объектов после уничтожения владеющего объекта
  • Использование перемещённых объектов

Решение: Следуйте правилам владения:

// Хорошо: Использовать заимствованные объекты в пределах времени жизни владельца
{
    example_plugin::KeyValues parent("Parent");
    auto child = parent.FindKey("Child");
    if (child) {
        child.SetName("NewName");
    }
} // Оба безопасно уничтожены

// Плохо: Заимствованный объект переживает владельца
example_plugin::KeyValues* dangling;
{
    example_plugin::KeyValues parent("Parent");
    auto child = parent.FindKey("Child");
    dangling = &child;
} // parent уничтожен, child теперь висячий!
// Использование dangling - неопределённое поведение

Невозможность копирования объектов

Проблема: Ошибка компилятора при попытке скопировать объекты.

Причина: Конструктор копирования и оператор присваивания копированием удалены для предотвращения двойного освобождения.

Решение: Используйте семантику перемещения или передавайте по ссылке:

// Неправильно: Невозможно скопировать
example_plugin::KeyValues kv1("Config");
example_plugin::KeyValues kv2 = kv1;  // ОШИБКА

// Правильно: Переместить
example_plugin::KeyValues kv2 = std::move(kv1);  // ОК

// Правильно: Передать по ссылке
void processConfig(const example_plugin::KeyValues& kv) { }
processConfig(kv1);  // ОК

Использование перемещённых объектов

Проблема: Методы выбрасывают "empty handle" после перемещения объекта.

Причина: После перемещения дескриптор исходного объекта устанавливается в nullptr.

Решение: Не используйте объекты после их перемещения:

example_plugin::KeyValues child("Child");
parent.AddSubKey(std::move(child));
// child теперь пуст!

// Неправильно: Использование перемещённого объекта
child.SetName("NewName");  // Выбрасывает: empty handle

// Правильно: Создать новый объект при необходимости
child = example_plugin::KeyValues("NewChild");
child.SetName("NewName");  // Теперь ОК

Утечки памяти

Проблема: Использование памяти растёт со временем.

Причина: Создание принадлежащих объектов без их уничтожения.

Решение: Позвольте RAII автоматически обрабатывать очистку:

// Плохо: Ручное управление
void* handle = example_plugin::Kv1Create("Config");
// ... забыть вызвать Kv1Destroy() ... УТЕЧКА!

// Хорошо: RAII обрабатывает это
void process() {
    example_plugin::KeyValues kv("Config");
    // ... использовать kv ...
} // Автоматически уничтожается - без утечки

// Хорошо: Явная очистка при необходимости
example_plugin::KeyValues kv("Config");
// ... использовать kv ...
kv.reset();  // Явная очистка

Преимущества RAII в C++

Классы C++ с RAII обеспечивают лучшее управление ресурсами среди всех языков Plugify:

  1. Автоматическая очистка: Специальный синтаксис не требуется (в отличие от defer в Go, using в C#)
  2. Детерминированность: Деструкторы выполняются немедленно, когда объекты выходят из области видимости
  3. Безопасность исключений: Ресурсы очищаются даже при возникновении исключений
  4. Нулевые накладные расходы: Семантика перемещения обеспечивает эффективную передачу владения
  5. Безопасность времени компиляции: Предотвращение копирования и проверка типов выявляют ошибки на ранней стадии
  6. Идиоматичность: RAII - стандартный паттерн C++ для управления ресурсами

Это делает C++ наиболее надёжным и эффективным языком для работы с классами Plugify.