c++virtualpybind11

pybind11 undefined behaviour with virtual function


I'm trying to use a simple virtual function but I'm running into some weird problems what seems like undefined behaviour. I have stripped everything from my code to only reproduce the problem:

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;

struct vec3
{
    double t, x, y;

    vec3(double t, double x, double y) : t(t), x(x), y(y) {}
};

class Point : public vec3
{

public:
    Point(double t, double x, double y) : vec3(t, x, y) {}

    virtual void test() {} // if this is virtual the problem occurs!
};

PYBIND11_MODULE(relatpy, m)
{
    py::class_<vec3>(m, "vec3")
        .def(py::init<double, double, double>())
        .def_readwrite("t", &vec3::t)
        .def_readwrite("x", &vec3::x)
        .def_readwrite("y", &vec3::y);

    py::class_<Point, vec3, PyPoint>(m, "Point")
        .def(py::init<double, double, double>());
}

The virtual function test doesn't do anything but if I remove the virtual the problem goes away. I have no idea why this is happening but when I run the following python code:

from relatpy import *

p = Point(1, 2, 3)  # t x y
print(p.t) 
print(p.x)
print(p.y)
# should print 1 2 3 but prints JUNK 1 2

as the comment says I expect 1, 2, 3 but get some junk, 1, 2. EDIT: As @273K pointed out in the comments this JUNK is the vtable pointer, although I'm not sure why it is ending up in the t variable. So any help on how to fix this is appreciated!

I expect the output 1 2 3 but only when test is virtual do I get "JUNK, 1, 2". I have tried implementing a trampoline class that implemented the virtual void test but this did not seem to solve the problem. Here is the code anyway in case I did something wrong:

for the trampoline class

class PyPoint : public Point
{
public:
    /* Inherit the constructors */
    using Point::Point;

    /* Trampoline (need one for each virtual function) */
    void test() override { PYBIND11_OVERRIDE_PURE(void, Point, test); }
};

and the bindings I changed:

py::class_<Point, vec3, PyPoint>(m, "Point")
        .def(py::init<double, double, double>())
        .def("test", &Point::test);

Solution

  • As @273K suggested in the comments I had to add t, x, y in de pybind bindings as follows:

        py::class_<Point, vec3, PyPoint>(m, "Point")
            .def(py::init<double, double, double>())
            .def("test", &Point::test);
            .def_readwrite("t", &Point::t)
            .def_readwrite("x", &Point::x)
            .def_readwrite("y", &Point::y);
    

    It seems both &Point::t and &vec3::t work, not sure if there is a difference.