Я использую pybind11 для создания сопоставлений Python для библиотеки C++ и использую pybind11::keep_alive для управления временем существования объектов C++, на которые ссылаются другие объекты. Это прекрасно работает, когда реферер является прямым возвращаемым значением, но у меня возникают проблемы, когда реферер является частью совокупного возвращаемого значения.
Вероятно, проще всего проиллюстрировать это упрощенным, но полным примером:
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
class Container {
public:
int some_value;
Container() {
std::cerr << "Container constructed\n";
some_value = 42;
}
~Container() {
some_value = -1;
std::cerr << "Container destructed\n";
}
// Container is not copyable.
Container(const Container &) = delete;
Container &operator=(const Container &) = delete;
// Iterator references the contents of Container.
struct Iterator {
Container *owner;
int value() const { return owner->some_value; }
};
Iterator iterator() { return Iterator{this}; }
std::pair<Iterator, bool> iterator_pair() { return {Iterator{this}, true}; }
};
PYBIND11_MODULE(example, module) {
py::class_<Container> owner(module, "Container");
py::class_<Container::Iterator>(owner, "Iterator")
.def_property_readonly("value", &Container::Iterator::value);
owner
.def(py::init())
.def("iterator", &Container::iterator, py::keep_alive<0, 1>())
.def("iterator_pair", &Container::iterator_pair, py::keep_alive<0, 1>());
}
В приведенном выше примере у меня есть объект-контейнер, который возвращает итератор, ссылающийся на содержимое контейнера, поэтому итератор должен поддерживать работоспособность контейнера. Это прекрасно работает для метода iterator, где py::keep_alive<0, 1> заставляет объект Iterator сохранять ссылку на объект Container, который его создал.
Однако это не работает для метода iterator_pair. Например, работая так:
import example
def Test1():
it = example.Container().iterator()
print(it.value) # prints 42
def Test2():
it, b = example.Container().iterator_pair()
assert b == True
print(it.value)
Test1() # OK
Test2() # Fails!
Ошибка со следующим выводом:
Owner constructed
42
Owner destructed
Owner constructed
Owner destructed
Traceback (most recent call last):
File "test.py", line 13, in <module>
Test2() # Fails!
^^^^^^^
File "test.py", line 8, in Test2
it, b = example.Container().iterator_pair()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: cannot create weak reference to 'tuple' object
И я не могу удалить py::keep_alive(0, 1), потому что это приведет к преждевременному уничтожению Контейнера:
Owner constructed
42
Owner destructed
Owner constructed
Owner destructed
269054512
(Обратите внимание, что 269054512 — это просто случайный мусор в памяти, поскольку объект Owner уже освобожден, когда на его значение ссылается итератор.)
Каков рекомендуемый способ действий в подобных ситуациях?
Я также сталкивался с аналогичными проблемами при возврате std::vectors объектов, которые должны поддерживать активность своего родителя, поэтому я думаю, что более общий вопрос заключается в том, как вернуть агрегатные объекты, содержащие подобъекты, которые должны поддерживать активность своего родителя.
Один из обходных путей, который я могу придумать, — это обернуть Container в std::shared_ptr<> и использовать его также в итераторе. Таким образом, объект C++ Container остается активным независимо от того, ссылается ли на него в Python. Однако мне не очень хочется это делать, потому что это требует значительного рефакторинга на стороне C++, и создается впечатление, что я дублирую поддержку подсчета ссылок, которая уже есть в Python.






Вы можете создать свою собственную версию keep_alive:
struct keep_alive_container {};
namespace pybind11::detail {
template <>
struct process_attribute<keep_alive_container>
: public process_attribute_default<keep_alive_container> {
static void precall(function_call&) {}
static void postcall(function_call& call, handle ret) {
keep_alive_impl(ret.attr("__getitem__")(0), call.args[0]);
}
};
}
// later on
.def("iterator_pair", &Container::iterator_pair, keep_alive_container())
Для этого необходимо вызвать keep_alive_impl, которая является частной pybind11 функцией, поэтому это не рекомендуется.
Спасибо! Это хорошее предложение, которое также можно адаптировать к различным другим случаям, которые я упомянул (например, std::vector<> объектов и т. д.). Немного грязно полагаться таким образом на детали реализации, но поскольку я начинаю подозревать строго внутри общедоступного API решения нет, я отмечу это как принятое решение.