Skip to content

04 — Virtual methods: overriding from Python

Source: examples/cookbook/04-virtuals.

To let a Python subclass override a C++ virtual, the binding needs a trampoline — a C++ subclass with one override per virtual that forwards into Python. welder automates everything around the trampoline (registration, static coverage checking, the dispatch plumbing), while the trampoline declarations themselves stay two mechanical lines each. Recipe 05 removes even those.

Base + trampoline

struct
[[=welder::weld(welder::lang::py), =welder::doc("A programmable robot.")]]
Robot {
    virtual ~Robot() = default;
    virtual std::string name() const { return "unit"; }
    virtual int speed() const { return 1; }

    // Bound FLAT: callable, but not overridable — no trampoline slot, no
    // C++->Python dispatch cost.
    [[=welder::rods::python::bind_flat]]
    virtual std::string vendor() const { return "ACME"; }

    // Non-virtual, calls the virtuals polymorphically: observing it from Python
    // proves C++ dispatches into the Python override.
    std::string status() const { return name() + " @ speed " + std::to_string(speed()); }
};

struct PyRobot : Robot {
    WELDER_PY_TRAMPOLINE(Robot);
    std::string name() const override { WELDER_PY_OVERRIDE(name); }
    int speed() const override { WELDER_PY_OVERRIDE(speed); }
};

The macros are backend-neutral — the same trampoline source compiles under the pybind11 or nanobind rod; only the included <welder/rods/python/<backend>/trampoline.hpp> differs.

Two discovery forms

welder must learn which trampoline serves which type. Explicit registration works anywhere (and disambiguates); the annotation form needs no registration but the trampoline must live in the type's namespace:

// explicit:
template <> constexpr std::meta::info
    welder::rods::python::trampoline_for<robots::Robot> = ^^robots::PyRobot;

// by annotation:
struct [[=welder::rods::python::trampoline]] PyMission : Mission { ... };

A welded type with virtuals and no trampoline is a compile error (mark the type or method bind_flat to opt out of overridability instead).

Abstract bases

Mission::duty() is pure virtual. welder registers the trampoline as the construction type, so Python subclasses instantiate and implement it; calling an unoverridden pure virtual raises RuntimeError at call time (framework behavior).

What the check asserts

A Python Scout(Robot) changes what the C++ status() returns — real C++ → Python dispatch; bind_flat methods stay callable but a Python override of them is invisible to C++; the abstract Mission is implementable from Python.