Заставить Boost Python не удалять объект C++ в деструкторе

Я создаю привязки для подмножества wxWidgets, используя Boost Python. Оконные объекты в wxWidgets не следует удалять вручную, так как они удаляют сами себя: например, когда окно верхнего уровня закрывается пользователем, нажав кнопку закрытия, оно автоматически удаляется. Если окно удалено, будут происходить странные вещи с обработчиками событий и т. д.

(Подробности: http://docs.wxwidgets.org/2.8/wx_windowdeletionoverview.html)

Однако это приводит к проблеме с объектами окон, созданными в Python: при сборке мусора объект C++ всегда удаляется!

Есть ли способ сказать Boost Python, чтобы он не брал на себя ответственность за объекты C++, которые он создает? Что-то вроде политики вызовов для конструктора?

(Кроме того, меня немного беспокоит, как обрабатывать объекты, удаленные из C++. Что должно произойти с объектом Python, когда связанный с ним объект C++ будет удален? Python никаким образом не получит уведомление об этом.)


person Jonatan    schedule 01.02.2013    source источник


Ответы (2)


Этого можно добиться с помощью boost::shared_ptr и некоторых настройка в Boost.Python.

boost::shared_ptr имеет конструкторы, которые принимают пользовательское средство удаления. Когда счетчик ссылок shared_ptr достигает нуля, shared_ptr вызывает средство удаления клиентов, передавая ранее управляемый указатель в качестве аргумента. Это позволяет использовать различные стратегии освобождения, такие как «без операции» или та, которая вызывает wxWindow::Destroy().

class window {};
void no_op(window*) {};
boost::shared_ptr(new window(), &no_op);

При предоставлении класса в Python Boost.Python позволяет управлять типами через файл HeldType. В этом случае HeldType будет boost::shared_ptr. Это позволяет правильно подсчитывать ссылки между C++ и Python и позволяет использовать собственную стратегию освобождения.

boost::python::class_<window, boost::shared_ptr<window>, ...>("Window", ...);

Хитрость в том, чтобы заставить их работать вместе прозрачно, заключается в следующем:

  • Не позволяйте Boost.Python создавать инициализатор по умолчанию (__init__).
  • Явно предоставьте функцию __init__, которая вызовет фабричную функцию, возвращающую shared_ptr с помощью пользовательского удаления.

Вот смоделированный класс window, который предназначен для уничтожения только через функцию-член destroy().

class window
{
...
private:
  ~window();
public:
  void destroy();
};

Определена фабричная функция, которая создаст объект window с подсчетом ссылок с пользовательским средством удаления. Пользовательский модуль удаления вызовет destroy() для объекта, когда счетчик ссылок достигнет нуля.

boost::shared_ptr<window> create_window()
{
  return boost::shared_ptr<window>(
    new window(),
    boost::mem_fn(&window::destroy));
}

Наконец, используйте Boost.Python для предоставления класса window, но отключите инициализатор по умолчанию и прозрачно замените его фабричной функцией create_window.

boost::python::class_<window, boost::shared_ptr<window>, 
                      boost::noncopyable>("Window", python::no_init)
  .def("__init__", python::make_constructor(&create_window));

Вот полный пример:

#include <iostream>
#include <boost/mem_fn.hpp>
#include <boost/python.hpp>
#include <boost/shared_ptr.hpp>

/// @brief Mockup window class.
class window
{
public:
  window(unsigned int id)
    : id_(id)
  {
    std::cout << "window::window() " << id_ << std::endl;
  }
  void action() { std::cout << "window::action() " << id_ << std::endl; }
  void destroy()
  {
    std::cout << "window::destroy() " << id_ << std::endl;
    delete this;
  }
private:
  ~window() { std::cout << "window::~window() " << id_ << std::endl; }
private:
  unsigned int id_;
};

/// @brief Factory function that will create reference counted window
///        objects, that will call window::destroy() when the reference
///        count reaches zero.
boost::shared_ptr<window> create_window(unsigned int id)
{
  return boost::shared_ptr<window>(
    new window(id),
    boost::mem_fn(&window::destroy));
}

BOOST_PYTHON_MODULE(example) {
  namespace python = boost::python;
  // Expose window, that will be managed by shared_ptr, and transparently
  // constructs the window via a factory function to allow for a custom
  // deleter.
  python::class_<window, boost::shared_ptr<window>, 
                 boost::noncopyable>("Window", python::no_init)
    .def("__init__", python::make_constructor(&create_window))
    .def("action", &window::action)
    ;
}

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

>>> from example import Window
>>> w1 = Window(1)
window::window() 1
>>> w2 = Window(2)
window::window() 2
>>> w3 = Window(3)
window::window() 3
>>> del w2
window::destroy() 2
window::~window() 2
>>> w3 = None
window::destroy() 3
window::~window() 3
>>> w = w1
>>> del w1
>>> w.action()
window::action() 1
>>> w = None
window::destroy() 1
window::~window() 1

Обратите внимание, что Python сообщает C++ об удалении объекта только после того, как у Python больше нет ссылки на экземпляр. Таким образом, в этом сценарии Python не будет пытаться взаимодействовать с удаленным объектом. Надеюсь, это уменьшит опасения, связанные с удалением объекта в C++.

Если есть ситуации, когда C++ будет удалять объекты, которые все еще активны в Python, рассмотрите возможность использования непрозрачного указателя, чтобы разделить класс реализации и класс дескриптора. Класс дескриптора может проверить, был ли удален связанный экземпляр реализации перед переадресацией вызова, позволяя передать исключение в Python.

person Tanner Sansbury    schedule 19.02.2013
comment
Вау, спасибо, много хороших идей. Я придумал другое решение, создав статический фабричный метод для каждого класса, который возвращает объект с return_value_policy reference_existing_object. Но ваше решение лучше. - person Jonatan; 20.02.2013
comment
Я попробовал этот подход с абстрактным классом, но обнаружил, что make_constructor тогда не работает. Если я использую конструктор init‹›, он работает, но если я использую make_constructor, реализации виртуальных методов теряются при передаче туда и обратно между Python и C++. Поскольку нет возможности установить собственный деструктор с помощью init‹›, я создал собственный класс shared_ptr из shared_ptr, который всегда имеет собственный деструктор. Но я не уверен, что безопасно получать от shared_ptr. Что вы думаете? - person Jonatan; 01.03.2013
comment
Я бы не решился вывести из shared_ptr. Концептуально это должно быть хорошо при использовании неполиморфным образом. Однако Boost.Python достаточно много метапрограммирует и обрабатывает для shared_ptr, поэтому я не могу с уверенностью сказать, что он безопасен. Исходная проблема звучит как результат нарезки объектов. Я ожидаю, что решение Boost.Python возможно с сочетанием bases и wrapper. - person Tanner Sansbury; 02.03.2013

На самом деле есть гораздо более простое решение с использованием auto_ptr, и оно даже в Часто задаваемые вопросы по Boost Python. Дополнительные сведения см. в вики-сайте Python. . Пока работает отлично.

person Jonatan    schedule 30.04.2013
comment
Пожалуйста, избегайте auto_ptr - person AKludges; 18.06.2020