Ответ 1
Из-за семантических различий между языками часто бывает сложно применить одно многоразовое решение для всех сценариев, когда задействованы коллекции. Самая большая проблема заключается в том, что в то время как коллекции Python напрямую поддерживают ссылки, коллекции С++ требуют уровня косвенности, например, с помощью типов элементов shared_ptr
. Без этой косвенности коллекции С++ не смогут поддерживать ту же функциональность, что и коллекции Python. Например, рассмотрим два индекса, которые относятся к одному и тому же объекту:
s = Spam()
spams = []
spams.append(s)
spams.append(s)
Без типов типа указателя коллекция С++ не может иметь двух индексов, относящихся к одному и тому же объекту. Тем не менее, в зависимости от использования и потребностей могут быть варианты, которые позволяют использовать интерфейс Pythonic-ish для пользователей Python, сохраняя при этом единственную реализацию для С++.
- Самое решение Pythonic - использовать пользовательский конвертер, который преобразует итеративный объект Python в коллекцию С++. См. этот ответ для деталей реализации. Рассмотрим этот вариант, если:
- Элементы коллекции дешевы для копирования.
- Функции С++ работают только на типах rvalue (т.е.
std::vector<>
илиconst std::vector<>&
). Это ограничение не позволяет С++ вносить изменения в коллекцию Python или ее элементы.
- Расширение
vector_indexing_suite
, повторное использование как можно большего числа возможностей, таких как его прокси-серверы для безопасного обращения с удалением индекса и перераспределением базового коллекция:- Представьте модель с пользовательским
HeldType
, который будет функционировать как интеллектуальный указатель и делегировать либо возвращенный объект экземпляра, либо объект-объект элемента отvector_indexing_suite
. - Monkey исправляет методы сбора, которые вставляют элементы в коллекцию, чтобы пользовательский
HeldType
был настроен на делегирование прокси-сервера элемента.
- Представьте модель с пользовательским
При экспонировании класса Boost.Python HeldType
является типом объекта, который внедряется в объект Boost.Python. При доступе к объекту завернутых типов Boost.Python вызывает get_pointer()
для HeldType
. Ниже приведен класс object_holder
, позволяющий возвращать дескриптор либо принадлежащему ему экземпляру, либо прокси-серверу элемента:
/// @brief smart pointer type that will delegate to a python
/// object if one is set.
template <typename T>
class object_holder
{
public:
typedef T element_type;
object_holder(element_type* ptr)
: ptr_(ptr),
object_()
{}
element_type* get() const
{
if (!object_.is_none())
{
return boost::python::extract<element_type*>(object_)();
}
return ptr_ ? ptr_.get() : NULL;
}
void reset(boost::python::object object)
{
// Verify the object holds the expected element.
boost::python::extract<element_type*> extractor(object_);
if (!extractor.check()) return;
object_ = object;
ptr_.reset();
}
private:
boost::shared_ptr<element_type> ptr_;
boost::python::object object_;
};
/// @brief Helper function used to extract the pointed to object from
/// an object_holder. Boost.Python will use this through ADL.
template <typename T>
T* get_pointer(const object_holder<T>& holder)
{
return holder.get();
}
При поддерживаемой косвенности остается только исправление коллекции для установки object_holder
. Один чистый и многоразовый способ поддержать это - использовать def_visitor
. Это общий интерфейс, который позволяет объектам class_
расширяться без вмешательства. Например, vector_indexing_suite
использует эту возможность.
Класс custom_vector_indexing_suite
ниже обезьяны исправляет метод append()
для делегирования исходному методу, а затем вызывает object_holder.reset()
прокси-сервер для вновь заданного элемента. Это приводит к тому, что object_holder
ссылается на элемент, содержащийся в коллекции.
/// @brief Indexing suite that will resets the element HeldType to
/// that of the proxy during element insertion.
template <typename Container,
typename HeldType>
class custom_vector_indexing_suite
: public boost::python::def_visitor<
custom_vector_indexing_suite<Container, HeldType>>
{
private:
friend class boost::python::def_visitor_access;
template <typename ClassT>
void visit(ClassT& cls) const
{
// Define vector indexing support.
cls.def(boost::python::vector_indexing_suite<Container>());
// Monkey patch element setters with custom functions that
// delegate to the original implementation then obtain a
// handle to the proxy.
cls
.def("append", make_append_wrapper(cls.attr("append")))
// repeat for __setitem__ (slice and non-slice) and extend
;
}
/// @brief Returned a patched 'append' function.
static boost::python::object make_append_wrapper(
boost::python::object original_fn)
{
namespace python = boost::python;
return python::make_function([original_fn](
python::object self,
HeldType& value)
{
// Copy into the collection.
original_fn(self, value.get());
// Reset handle to delegate to a proxy for the newly copied element.
value.reset(self[-1]);
},
// Call policies.
python::default_call_policies(),
// Describe the signature.
boost::mpl::vector<
void, // return
python::object, // self (collection)
HeldType>() // value
);
}
};
Планирование должно происходить во время выполнения, а пользовательские объекты-объекты не могут быть непосредственно определены в классе через def()
, поэтому make_function()
функция должна использоваться. Для функторов он требует CallPolicies и MPL front- расширяемая последовательность, представляющая подпись.
Вот полный пример: демонстрирует с помощью object_holder
для делегирования прокси и custom_vector_indexing_suite
для исправления коллекции.
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
/// @brief Mockup type.
struct spam
{
int val;
spam(int val) : val(val) {}
bool operator==(const spam& rhs) { return val == rhs.val; }
};
/// @brief Mockup function that operations on a collection of spam instances.
void modify_spams(std::vector<spam>& spams)
{
for (auto& spam : spams)
spam.val *= 2;
}
/// @brief smart pointer type that will delegate to a python
/// object if one is set.
template <typename T>
class object_holder
{
public:
typedef T element_type;
object_holder(element_type* ptr)
: ptr_(ptr),
object_()
{}
element_type* get() const
{
if (!object_.is_none())
{
return boost::python::extract<element_type*>(object_)();
}
return ptr_ ? ptr_.get() : NULL;
}
void reset(boost::python::object object)
{
// Verify the object holds the expected element.
boost::python::extract<element_type*> extractor(object_);
if (!extractor.check()) return;
object_ = object;
ptr_.reset();
}
private:
boost::shared_ptr<element_type> ptr_;
boost::python::object object_;
};
/// @brief Helper function used to extract the pointed to object from
/// an object_holder. Boost.Python will use this through ADL.
template <typename T>
T* get_pointer(const object_holder<T>& holder)
{
return holder.get();
}
/// @brief Indexing suite that will resets the element HeldType to
/// that of the proxy during element insertion.
template <typename Container,
typename HeldType>
class custom_vector_indexing_suite
: public boost::python::def_visitor<
custom_vector_indexing_suite<Container, HeldType>>
{
private:
friend class boost::python::def_visitor_access;
template <typename ClassT>
void visit(ClassT& cls) const
{
// Define vector indexing support.
cls.def(boost::python::vector_indexing_suite<Container>());
// Monkey patch element setters with custom functions that
// delegate to the original implementation then obtain a
// handle to the proxy.
cls
.def("append", make_append_wrapper(cls.attr("append")))
// repeat for __setitem__ (slice and non-slice) and extend
;
}
/// @brief Returned a patched 'append' function.
static boost::python::object make_append_wrapper(
boost::python::object original_fn)
{
namespace python = boost::python;
return python::make_function([original_fn](
python::object self,
HeldType& value)
{
// Copy into the collection.
original_fn(self, value.get());
// Reset handle to delegate to a proxy for the newly copied element.
value.reset(self[-1]);
},
// Call policies.
python::default_call_policies(),
// Describe the signature.
boost::mpl::vector<
void, // return
python::object, // self (collection)
HeldType>() // value
);
}
// .. make_setitem_wrapper
// .. make_extend_wrapper
};
BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;
// Expose spam. Use a custom holder to allow for transparent delegation
// to different instances.
python::class_<spam, object_holder<spam>>("Spam", python::init<int>())
.def_readwrite("val", &spam::val)
;
// Expose a vector of spam.
python::class_<std::vector<spam>>("SpamVector")
.def(custom_vector_indexing_suite<
std::vector<spam>, object_holder<spam>>())
;
python::def("modify_spams", &modify_spams);
}
Интерактивное использование:
>>> import example
>>> spam = example.Spam(5)
>>> spams = example.SpamVector()
>>> spams.append(spam)
>>> assert(spams[0].val == 5)
>>> spam.val = 21
>>> assert(spams[0].val == 21)
>>> example.modify_spams(spams)
>>> assert(spam.val == 42)
>>> spams.append(spam)
>>> spam.val = 100
>>> assert(spams[1].val == 100)
>>> assert(spams[0].val == 42) # The container does not provide indirection.
Пока используется vector_indexing_suite
, базовый контейнер С++ должен быть модифицирован только с помощью API-интерфейса Python. Например, вызов push_back
в контейнере может привести к перераспределению базовой памяти и вызвать проблемы с существующими прокси-серверами Boost.Python. С другой стороны, можно безопасно модифицировать сами элементы, например, с помощью функции modify_spams()
выше.