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

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

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

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

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

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

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

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

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

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

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

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

Генератор анализирует манифест вашего плагина и создаёт обёртки классов C++ для объектов, имеющих функции конструктора и деструктора.

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

{
  "fileVersion": 1,
  "version": 1,
  "versionName": "1.0",
  "friendlyName": "Test KeyValues Plugin",
  "description": "Пример плагина с классом KeyValues",
  "createdBy": "Untrusted Modders Team",
  "createdByURL": "https://github.com/untrustedmodders",
  "docsURL": "https://github.com/untrustedmodders/plugify/wiki",
  "downloadURL": "https://github.com/untrustedmodders/plugify-module-cpp",
  "updateURL": "https://raw.githubusercontent.com/untrustedmodders/plugify-module-cpp/main/.github/plugify-module-cpp.json",
  "exportedMethods": [
    {
      "name": "Kv1Create",
      "paramTypes": ["string"],
      "retType": {
        "type": "ptr64",
        "ref": "kv1"
      },
      "funcName": "Kv1Create"
    },
    {
      "name": "Kv1Destroy",
      "paramTypes": [
        {
          "type": "ptr64",
          "ref": "kv1"
        }
      ],
      "funcName": "Kv1Destroy"
    },
    {
      "name": "Kv1GetName",
      "paramTypes": [
        {
          "type": "ptr64",
          "ref": "kv1"
        }
      ],
      "retType": "string",
      "funcName": "Kv1GetName"
    },
    {
      "name": "Kv1SetName",
      "paramTypes": [
        {
          "type": "ptr64",
          "ref": "kv1"
        },
        "string"
      ],
      "funcName": "Kv1SetName"
    },
    {
      "name": "Kv1FindKey",
      "paramTypes": [
        {
          "type": "ptr64",
          "ref": "kv1"
        },
        "string"
      ],
      "retType": {
        "type": "ptr64",
        "ref": "kv1",
        "owner": false
      },
      "funcName": "Kv1FindKey"
    },
    {
      "name": "Kv1AddSubKey",
      "paramTypes": [
        {
          "type": "ptr64",
          "ref": "kv1"
        },
        {
          "type": "ptr64",
          "ref": "kv1",
          "owner": true
        }
      ],
      "funcName": "Kv1AddSubKey"
    }
  ],
  "dependencies": [],
  "entryPoint": "test_keyvalues"
}

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

  1. Имя ссылки (ref): Все связанные функции используют одно и то же значение ref (например, "kv1"), чтобы сгруппировать их вместе
  2. Шаблон конструктора: Функция, возвращающая {"type": "ptr64", "ref": "kv1"}, становится конструктором класса
  3. Шаблон деструктора: Функция, принимающая {"type": "ptr64", "ref": "kv1"} и ничего не возвращающая, становится деструктором
  4. Владение (owner): Контролирует, берёт ли класс владение параметром или возвращаемым значением

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

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

namespace test_keyvalues {

  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)
      : _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 test_keyvalues

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

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

get()

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

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

release()

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

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

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

reset()

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

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

swap()

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

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

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

operator bool()

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

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

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

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

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

test_keyvalues::KeyValues kv1("Config1");
test_keyvalues::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() {
    test_keyvalues::KeyValues kv("ServerConfig");
    kv.SetName("Production");
    // Ресурс автоматически уничтожается, когда kv выходит из области видимости
} // Деструктор вызывается здесь - Kv1Destroy() вызывается автоматически

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

enum class Ownership : bool { Borrowed, Owned };

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

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

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

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

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

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

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

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

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

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

test_keyvalues::KeyValues parent("Parent");
test_keyvalues::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 = getRawHandleFromSomewhere();

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

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

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

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

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

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

PLUGIN_EXPORT void pluginStart() {
    g_config = test_keyvalues::KeyValues("GlobalConfig");
    // ... использовать config
}

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

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

Деструкторы C++ для глобальных объектов выполняются после выгрузки плагина:

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

Решения

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

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

PLUGIN_EXPORT void pluginStart() {
    test_keyvalues::KeyValues config("Config");
    // Использовать config...
} // Автоматически уничтожается до выгрузки плагина

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

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

test_keyvalues::KeyValues g_config;

PLUGIN_EXPORT void pluginEnd() {
    g_config.reset();  // Уничтожить СЕЙЧАС, не позже
}

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

#include <memory>

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

PLUGIN_EXPORT void pluginStart() {
    g_config = std::make_unique<test_keyvalues::KeyValues>("Config");
}

PLUGIN_EXPORT void pluginEnd() {
    g_config.reset();  // Уничтожить и установить в nullptr
}

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

#include <optional>

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

PLUGIN_EXPORT void pluginStart() {
    g_config.emplace("Config");
}

PLUGIN_EXPORT void pluginEnd() {
    g_config.reset();  // Уничтожить содержащийся объект
}

Поля класса

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

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

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

private:
    test_keyvalues::KeyValues m_config;
};

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

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

#include <plugify/plugify.hpp>
#include <test_keyvalues.hpp>
#include <iostream>

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

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

        auto caching = test_keyvalues::KeyValues("Caching");
        caching.SetName("Redis");
        m_rootConfig.AddSubKey(std::move(caching));
    }

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

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

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

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

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

private:
    test_keyvalues::KeyValues m_rootConfig;
};

ConfigManager g_manager;

PLUGIN_EXPORT void pluginStart() {
    try {
        g_manager.Initialize();
        g_manager.ProcessConfig();
        g_manager.UpdateConfig();
    } catch (const std::exception& e) {
        std::cerr << "Ошибка: " << e.what() << std::endl;
        // Даже если выброшено исключение, объекты очищаются
    }
}

PLUGIN_EXPORT void pluginEnd() {
    // КРИТИЧНО: Очистить перед выгрузкой!
    g_manager.Cleanup();
}

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

  1. Предпочитайте локальные переменные: Используйте RAII с локальными переменными для автоматической очистки
    void process() {
        test_keyvalues::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() в pluginEnd() для глобальных/статических объектов
    PLUGIN_EXPORT void pluginEnd() {
        g_config.reset();
    }
    
  5. Не используйте перемещённые объекты: После перемещения объекты пусты
    test_keyvalues::KeyValues kv("Config");
    auto kv2 = std::move(kv);
    // kv теперь пуст - не используйте его!
    
  6. Используйте const ссылки для доступа только для чтения: Избегайте ненужной передачи владения
    void readConfig(const test_keyvalues::KeyValues& kv) {
        auto name = kv.GetName();
    }
    
  7. Используйте умные указатели для опционального владения: Когда владение динамическое, используйте std::unique_ptr
    std::unique_ptr<test_keyvalues::KeyValues> maybeConfig;
    if (needConfig) {
        maybeConfig = std::make_unique<test_keyvalues::KeyValues>("Config");
    }
    
  8. Используйте безопасность исключений: RAII обеспечивает автоматическую очистку при исключениях
    void riskyOperation() {
        test_keyvalues::KeyValues kv("Config");
        mightThrow();  // kv всё равно уничтожится, даже если это выбросит исключение
    }
    

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

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

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

Причины:

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

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

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

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

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

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

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

PLUGIN_EXPORT void pluginEnd() {
    g_config.reset();
    g_manager.Cleanup();
}

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

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

Причины:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Хорошо: Явная очистка при необходимости
test_keyvalues::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.