Узнайте, как использовать обертки классов для более чистых 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); // Легко забыть!
Однако у этого подхода есть несколько проблем:
Утечки ресурсов: Если вы забудете вызвать Kv1Destroy(), ресурс утечёт
Небезопасность при исключениях: Если возникнет исключение, деструктор не будет вызван
Отсутствие типобезопасности: Сырые указатели void* не обеспечивают проверку типов во время компиляции
Многословность: Вы должны вручную передавать дескриптор каждому вызову функции
Подверженность ошибкам: Легко использовать дескриптор после его уничтожения
Классы решают все эти проблемы, используя RAII C++ (Resource Acquisition Is Initialization):
// Подход RAII - автоматический, безопасный, устойчивый к исключениям
test_keyvalues::KeyValues kv("Config");
kv.SetName("ServerConfig");
auto name = kv.GetName();
// Автоматически уничтожается, когда kv выходит из области видимости!
Возвращает сырой дескриптор без передачи владения. Используйте это, когда вам нужно передать дескриптор C-стиль функциям, которые не берут владение:
test_keyvalues::KeyValues kv("Config");
void* raw_handle = kv.get();
// kv по-прежнему владеет дескриптором - уничтожит его, когда kv выйдет из области видимости
Передаёт владение вызывающей стороне и очищает внутренний дескриптор. Используйте это, когда функция берёт владение объектом:
test_keyvalues::KeyValues subKey("SubSection");
parent.AddSubKey(std::move(subKey)); // AddSubKey внутренне вызывает release()
// После этого вызова:
// - parent теперь владеет ресурсом subKey
// - объект subKey перемещён и его дескриптор освобождён
// - Когда parent будет уничтожен, он также уничтожит subKey
test_keyvalues::KeyValues kv1("Config1");
test_keyvalues::KeyValues kv2("Config2");
kv1.swap(kv2); // Теперь kv1 имеет Config2, kv2 имеет Config1
// Или используя std::swap:
std::swap(kv1, kv2);
Сравнивают экземпляры по значениям их дескрипторов:
test_keyvalues::KeyValues kv1("Config1");
test_keyvalues::KeyValues kv2("Config2");
if (kv1 == kv2) { // Сравнение по дескриптору
// Один и тот же базовый объект
}
if (kv1 != kv2) {
// Разные объекты
}
// Оператор spaceship для упорядочивания
if (kv1 < kv2) {
// Дескриптор kv1 меньше, чем у kv2
}
Классы 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++: Автоматически - специальный синтаксис не требуется!
Классы предотвращают копирование во избежание ошибок двойного освобождения:
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); // ОК
Когда метод возвращает указатель, которым вы не владеете (помечен "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++ для глобальных объектов выполняются после выгрузки плагина:
Вызывается pluginEnd()
Общая библиотека вашего плагина выгружается из памяти
Вызываются глобальные деструкторы
Глобальный деструктор пытается вызвать Kv1Destroy() - но функция исчезла!
Проблема: Сбой или неопределённое поведение при использовании объектов.
Причины:
Попытка скопировать объекты (конструктор копирования удалён, поэтому это не скомпилируется)
Использование заимствованных объектов после уничтожения владеющего объекта
Использование перемещённых объектов
Решение: Следуйте правилам владения:
// Хорошо: Использовать заимствованные объекты в пределах времени жизни владельца
{
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 - неопределённое поведение
Проблема: Методы выбрасывают "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"); // Теперь ОК