This can be accomplished with a boost::shared_ptr
and some setup in Boost.Python.
boost::shared_ptr
has constructors that accept a custom deleter. When the shared_ptr
's reference count reaches zero, the shared_ptr
will invoke the customer deleter, passing the previously managed pointer as an argument. This allows for different deallocation strategies to be used, such as a "no-op", or one that invokes wxWindow::Destroy()
.
class window {};
void no_op(window*) {};
boost::shared_ptr(new window(), &no_op);
When exposing a class to Python, Boost.Python allows for types to be managed via a HeldType
. In this case, the HeldType
will be boost::shared_ptr
. This allows for proper referencing counting to occur between C++ and Python, and allows for the custom deallocation strategy.
boost::python::class_<window, boost::shared_ptr<window>, ...>("Window", ...);
The trick to getting these to work together transparently is to:
- Suppress Boost.Python from creating a default initializer (
__init__
).
- Explicitly provide an
__init__
function that will invoke a factory function returning a shared_ptr
with a custom deleter.
Here is the a mocked up window
class that is intended to only be destroyed through the destroy()
member function.
class window
{
...
private:
~window();
public:
void destroy();
};
A factory function is defined that will create a reference counted window
object with a custom deleter. The custom deleter will invoke destroy()
on the object when the reference count reaches zero.
boost::shared_ptr<window> create_window()
{
return boost::shared_ptr<window>(
new window(),
boost::mem_fn(&window::destroy));
}
Finally, use Boost.Python to expose the window
class, but suppress the default initializer, and transparently replace it with the create_window
factory function.
boost::python::class_<window, boost::shared_ptr<window>,
boost::noncopyable>("Window", python::no_init)
.def("__init__", python::make_constructor(&create_window));
Here is a complete example:
#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)
;
}
And its usage:
>>> 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
Notice how Python only informs C++ to delete the object once Python no longer has a reference to the instance. Thus, in this scenario, Python will not try to interact on an object that has been deleted. Hopefully this alleviates concerns expressed when an object is deleted in C++.
If there are situations where C++ will be deleting objects that are still active in Python, then consider using an opaque pointer to separate the implementation class and the handle class. The handle class could check if the associated implementation instance has been deleted before forwarding the call, allowing an exception to be thrown up to Python.