Skip to content

[BUG]: Recursive dispatch fails to find python function override.  #3357

Open
@dyershov

Description

@dyershov

Required prerequisites

Problem description

Consider a simple C++ visitor pattern:

class Data{};
using DataVisitor = std::function<void (const Data&)>;

class Adder {
public:
    // interface to add two data objects, the result is passes to data visitor
    virtual void operator()(const Data& first, const Data& second, const DataVisitor& visitor) const = 0;
};

void add(const Data& first, const Data& second,  const Adder& adder, const DataVisitor& visitor) {
    adder(first, second, visitor);
}

void add3(const Data& first, const Data& second, const Data& third,  const Adder& adder, const DataVisitor& visitor) {
    adder(first, second, [&] (const Data& first_plus_second) {
        adder(first_plus_second, third, visitor);
    });
}

In this pattern, Data and Adder are abstract classes which can be used in two algorithms add and add3 which, as the name suggest, add two or three objects of the type Data and send the result to a visitor. This pattern is intended to eliminate allocating memory for Data pointer in case we need to store temporary results, e.g., see add3 algorithm.

Next, we define the corresponding python bindings:

class PyAdder : public Adder {
public:
    void operator()(const Data& first, const Data& second, const DataVisitor& visitor) const override {
        PYBIND11_OVERRIDE_PURE_NAME(void, Adder, "__call__", operator(), first, second, visitor);
    }
};

class PyData : public Data {
public:
    PyData() = default;
};

PYBIND11_MODULE(module, m) {
    pybind11::class_<Adder, PyAdder>(m, "Adder")
        .def(pybind11::init<>())
        .def("__call__", &Adder::operator());

    m.def("add", &add);
    m.def("add3", &add3);

    pybind11::class_<Data, PyData>(m, "Data")
        .def(pybind11::init<>());
}

Finally, let's test it in Python. Firs,t some boilerplate:

class Adder(module.Adder):
    def __call__(self, first, second, visitor):
        visitor(Data(first.value + second.value))

class Data(module.Data):
    def __init__(self, value):
        super().__init__()
        self.value = value

def print_result(data):
        print(data.value)

Testing function add yields the expected result

module.add(Data(1), Data(2), Adder(), print_result) # prints 3

However, testing add3 causes failure

module.add3(Data(1), Data(2), Data(3), Adder(), print_result) # raises RuntimeError

The error reads

Traceback (most recent call last):
  File "*****/test_add.py", line 39, in test_add3_int
    module.add3(Data(1), Data(2), Data(3), Adder(), print_result)
  File "*****/test_add.py", line 8, in __call__
    visitor(Data(first.value + second.value))
RuntimeError: Tried to call pure virtual function "Adder::__call__"

Chaising the problem through pybind11 sources, I found a possible culprit. In function get_type_override, we check if dispatch code is invoked from overridden function.

    /* Don't call dispatch code if invoked from overridden function.
       Unfortunately this doesn't work on PyPy. */
#if !defined(PYPY_VERSION)
    PyFrameObject *frame = PyThreadState_Get()->frame;
    if (frame != nullptr && (std::string) str(frame->f_code->co_name) == name
        && frame->f_code->co_argcount > 0) {
        PyFrame_FastToLocals(frame);
        PyObject *self_caller = dict_getitem(
            frame->f_locals, PyTuple_GET_ITEM(frame->f_code->co_varnames, 0));
        if (self_caller == self.ptr())
            return function();
    }
#else

It returns an empty function if the second override is called from the same frame as the first override. Commenting this check renders a working example. I consider this as a bug, but please let consider proving me wrong.

My questions are:

  • In which case it is important to prevent the second dispatch from overridden function?
  • Is there a good way to fix things related to Visitor (recursive) or Double Dispatch patterns?

The link for reproducible and extended examples: https://bitbucket.org/yershov/recursive-dispatch/src/main/

Reproducible example code

https://bitbucket.org/yershov/recursive-dispatch/src/main/

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions