pybind11 icon indicating copy to clipboard operation
pybind11 copied to clipboard

[QUESTION]Can different return value and call policies be used depending on the actual type of std::variant?

Open HunterTracer opened this issue 4 years ago • 1 comments

Recently, I want to export a function whose return type is std::variant. However, different return value policies and additional call policies should be applied according to the actual return value type, for example: When return type is A, it needs to be py::return_value_policy::copy. When return type is B, it needs to be py::return_value_policy::take_ownership with additional call policy py::keep_alive<0, 1>(). When return type is C, it needs to be py::return_value_policy::reference_internal.

Are there any ways to specify the different return_value_policies for each return type of std::variant<...>? Many thanks!

HunterTracer avatar Jul 10 '21 12:07 HunterTracer


#include <pybind11/pybind11.h>
#include <variant>
#include <string>
#include <memory>

namespace py = pybind11;

// Example classes/structs for A, B, C
struct A {
    int value;
    A(int v) : value(v) {}
};

struct B {
    std::unique_ptr<int> ptr;
    B(int v) : ptr(std::make_unique<int>(v)) {}
};

struct C {
    std::string text;
    C(const std::string& t) : text(t) {}
};

// The original function returning a std::variant
std::variant<A, B, C> get_variant(int choice) {
    if (choice == 0) return A{42};           // Returns A
    if (choice == 1) return B{100};          // Returns B (owns a unique_ptr)
    return C{"Hello, Python!"};              // Returns C
}

// Wrapper function to apply dynamic return value policies
py::object get_variant_wrapper(int choice) {
    auto result = get_variant(choice);

    return std::visit(
        [](auto&& arg) -> py::object {
            using T = std::decay_t<decltype(arg)>;

            // Case 1: Type A - Use copy
            if constexpr (std::is_same_v<T, A>) {
                return py::cast(arg, py::return_value_policy::copy);
            }
            // Case 2: Type B - Use take_ownership with keep_alive<0, 1>
            else if constexpr (std::is_same_v<T, B>) {
                // Move the unique_ptr to Python, transferring ownership
                auto obj = py::cast(std::move(arg.ptr), py::return_value_policy::take_ownership);
                return obj.attr("self") = py::cast(&arg, py::return_value_policy::move)
                    .with_(py::keep_alive<0, 1>()); // Keep B alive as long as the returned object is
            }
            // Case 3: Type C - Use reference_internal
            else if constexpr (std::is_same_v<T, C>) {
                return py::cast(arg, py::return_value_policy::reference_internal);
            }
            // Should never reach here due to variant completeness
            throw std::runtime_error("Unknown variant type");
        },
        result
    );
}

// pybind11 module definition
PYBIND11_MODULE(example, m) {
    // Bind the structs
    py::class_<A>(m, "A")
        .def(py::init<int>())
        .def_readwrite("value", &A::value);

    py::class_<B>(m, "B")
        .def(py::init<int>())
        .def_readwrite("ptr", &B::ptr);

    py::class_<C>(m, "C")
        .def(py::init<std::string>())
        .def_readwrite("text", &C::text);

    // Bind the wrapper function
    m.def("get_variant", &get_variant_wrapper, "Get a variant with dynamic return policy",
          py::arg("choice"));
}

​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​

ljluestc avatar Mar 12 '25 00:03 ljluestc