Add type caster for std::variant and other variant-like classes

This commit is contained in:
Dean Moldovan 2017-04-21 23:54:41 +02:00
parent a01b6b805c
commit 4ffa76ec56
5 changed files with 142 additions and 0 deletions

View File

@ -144,6 +144,8 @@ as arguments and return values, refer to the section on binding :ref:`classes`.
+------------------------------------+---------------------------+-------------------------------+
| ``std::experimental::optional<T>`` | STL optional type (exp.) | :file:`pybind11/stl.h` |
+------------------------------------+---------------------------+-------------------------------+
| ``std::variant<...>`` | Type-safe union (C++17) | :file:`pybind11/stl.h` |
+------------------------------------+---------------------------+-------------------------------+
| ``std::function<...>`` | STL polymorphic function | :file:`pybind11/functional.h` |
+------------------------------------+---------------------------+-------------------------------+
| ``std::chrono::duration<...>`` | STL time duration | :file:`pybind11/chrono.h` |

View File

@ -26,6 +26,51 @@ next sections for more details and alternative approaches that avoid this.
The file :file:`tests/test_python_types.cpp` contains a complete
example that demonstrates how to pass STL data types in more detail.
C++17 library containers
========================
The :file:`pybind11/stl.h` header also includes support for ``std::optional<>``
and ``std::variant<>``. These require a C++17 compiler and standard library.
In C++14 mode, ``std::experimental::optional<>`` is supported if available.
Various versions of these containers also exist for C++11 (e.g. in Boost).
pybind11 provides an easy way to specialize the ``type_caster`` for such
types:
.. code-block:: cpp
// `boost::optional` as an example -- can be any `std::optional`-like container
namespace pybind11 { namespace detail {
template <typename T>
struct type_caster<boost::optional<T>> : optional_caster<boost::optional<T>> {};
}}
The above should be placed in a header file and included in all translation units
where automatic conversion is needed. Similarly, a specialization can be provided
for custom variant types:
.. code-block:: cpp
// `boost::variant` as an example -- can be any `std::variant`-like container
namespace pybind11 { namespace detail {
template <typename... Ts>
struct type_caster<boost::variant<Ts...>> : variant_caster<boost::variant<Ts...>> {};
// Specifies the function used to visit the variant -- `apply_visitor` instead of `visit`
template <>
struct visit_helper<boost::variant> {
template <typename... Args>
static auto call(Args &&...args)
-> decltype(boost::apply_visitor(std::forward<Args>(args)...)) {
return boost::apply_visitor(std::forward<Args>(args)...);
}
};
}} // namespace pybind11::detail
The ``visit_helper`` specialization is not required if your ``name::variant`` provides
a ``name::visit()`` function. For any other function name, the specialization must be
included to tell pybind11 how to visit the variant.
.. _opaque:
Making opaque types

View File

@ -36,6 +36,11 @@
# define PYBIND11_HAS_EXP_OPTIONAL 1
# endif
# endif
// std::variant
# if defined(PYBIND11_CPP17) && __has_include(<variant>)
# include <variant>
# define PYBIND11_HAS_VARIANT 1
# endif
#endif
NAMESPACE_BEGIN(pybind11)
@ -262,6 +267,66 @@ template<> struct type_caster<std::experimental::nullopt_t>
: public void_caster<std::experimental::nullopt_t> {};
#endif
/// Visit a variant and cast any found type to Python
struct variant_caster_visitor {
return_value_policy policy;
handle parent;
template <typename T>
handle operator()(T &&src) const {
return make_caster<T>::cast(std::forward<T>(src), policy, parent);
}
};
/// Helper class which abstracts away variant's `visit` function. `std::variant` and similar
/// `namespace::variant` types which provide a `namespace::visit()` function are handled here
/// automatically using argument-dependent lookup. Users can provide specializations for other
/// variant-like classes, e.g. `boost::variant` and `boost::apply_visitor`.
template <template<typename...> class Variant>
struct visit_helper {
template <typename... Args>
static auto call(Args &&...args) -> decltype(visit(std::forward<Args>(args)...)) {
return visit(std::forward<Args>(args)...);
}
};
/// Generic variant caster
template <typename Variant> struct variant_caster;
template <template<typename...> class V, typename... Ts>
struct variant_caster<V<Ts...>> {
static_assert(sizeof...(Ts) > 0, "Variant must consist of at least one alternative.");
template <typename U, typename... Us>
bool load_alternative(handle src, bool convert, type_list<U, Us...>) {
auto caster = make_caster<U>();
if (caster.load(src, convert)) {
value = cast_op<U>(caster);
return true;
}
return load_alternative(src, convert, type_list<Us...>{});
}
bool load_alternative(handle, bool, type_list<>) { return false; }
bool load(handle src, bool convert) {
return load_alternative(src, convert, type_list<Ts...>{});
}
template <typename Variant>
static handle cast(Variant &&src, return_value_policy policy, handle parent) {
return visit_helper<V>::call(variant_caster_visitor{policy, parent},
std::forward<Variant>(src));
}
using Type = V<Ts...>;
PYBIND11_TYPE_CASTER(Type, _("Union[") + detail::concat(make_caster<Ts>::name()...) + _("]"));
};
#if PYBIND11_HAS_VARIANT
template <typename... Ts>
struct type_caster<std::variant<Ts...>> : variant_caster<std::variant<Ts...>> { };
#endif
NAMESPACE_END(detail)
inline std::ostream &operator<<(std::ostream &os, const handle &obj) {

View File

@ -354,6 +354,23 @@ test_initializer python_types([](py::module &m) {
m.attr("has_optional") = has_optional;
m.attr("has_exp_optional") = has_exp_optional;
#ifdef PYBIND11_HAS_VARIANT
struct visitor {
const char *operator()(int) { return "int"; }
const char *operator()(std::string) { return "std::string"; }
const char *operator()(double) { return "double"; }
};
m.def("load_variant", [](std::variant<int, std::string, double> v) {
return std::visit(visitor(), v);
});
m.def("cast_variant", []() {
using V = std::variant<int, std::string>;
return py::make_tuple(V(5), V("Hello"));
});
#endif
m.def("test_default_constructors", []() {
return py::dict(
"str"_a=py::str(),

View File

@ -1,5 +1,6 @@
# Python < 3 needs this: coding=utf-8
import pytest
import pybind11_tests
from pybind11_tests import ExamplePythonTypes, ConstructorStats, has_optional, has_exp_optional
@ -370,6 +371,18 @@ def test_exp_optional():
assert test_nullopt_exp(43) == 43
@pytest.mark.skipif(not hasattr(pybind11_tests, "load_variant"), reason='no <variant>')
def test_variant(doc):
from pybind11_tests import load_variant, cast_variant
assert load_variant(1) == "int"
assert load_variant("1") == "std::string"
assert load_variant(1.0) == "double"
assert cast_variant() == (5, "Hello")
assert doc(load_variant) == "load_variant(arg0: Union[int, str, float]) -> str"
def test_constructors():
"""C++ default and converting constructors are equivalent to type calls in Python"""
from pybind11_tests import (test_default_constructors, test_converting_constructors,