Alternatives to virtual_ptr

Virtual arguments can also be passed as plain references, plain pointers, or smart pointers, i.e. without using virtual_ptr. In a method declaration, parameters with a type wrappted in class template virtual_ are considered in overrider selection (along with virtual_ptr parameters).

For example, the Node example can be rewritten as follows:

struct Node {
    virtual int value() const = 0;
};

struct Variable : Node {
    Variable(int value) : v(value) {}
    int value() const override { return v; }
    int v;
};

struct Plus : Node {
    Plus(const Node& left, const Node& right) : left(left), right(right) {}
    int value() const override { return left.value() + right.value(); }
    const Node& left; const Node& right;
};

struct Times : Node {
    Times(const Node& left, const Node& right) : left(left), right(right) {}
    int value() const override { return left.value() * right.value(); }
    const Node& left; const Node& right;
};

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

using boost::openmethod::virtual_;

BOOST_OPENMETHOD(postfix, (virtual_<const Node&> node, std::ostream& os), void);

BOOST_OPENMETHOD_OVERRIDE(
    postfix, (const Variable& var, std::ostream& os), void) {
    os << var.v;
}

BOOST_OPENMETHOD_OVERRIDE(
    postfix, (const Plus& plus, std::ostream& os), void) {
    postfix(plus.left, os);
    os << ' ';
    postfix(plus.right, os);
    os << " +";
}

BOOST_OPENMETHOD_OVERRIDE(
    postfix, (const Times& times, std::ostream& os), void) {
    postfix(times.left, os);
    os << ' ';
    postfix(times.right, os);
    os << " *";
}

BOOST_OPENMETHOD_CLASSES(Node, Variable, Plus, Times);

auto main() -> int {
    boost::openmethod::initialize();
    Variable a{2}, b{3}, c{4};
    Plus d{a, b};
    Times e{d, c};
    postfix(e, std::cout);
    std::cout << " = " << e.value() << "\n"; // 2 3 + 4 * = 20
}

Note that virtual_ is not used in the overrider. It is just a decorator used in the method. It is also removed from the method’s signature. BOOST_OPENMETHOD generates:

inline auto postfix(const Node& node, std::ostream& os) -> void {
    // examine the type of node
    // select an overrider
    // call it
}

boost_openmethod_vptr

By itself, virtual_ does not provide any benefits. Passing the virtual argument by reference almost compiles to the same code as creating a virtual_ptr, using it for one call, then throwing it way. The only difference is that the virtual argument is passed as one pointer instead of two.

However, we can now customize how the vptr is obtained. When the method sees a virtual_ parameter, it looks for a boost_openmethod_vptr function that can be called with the virtual argument (passed by const reference) and a pointer to a registry, and returns a vptr_type. If one is found, it is called to acquire the vptr.

In the following example, we embed the vptr in the object, just like the vptr for native virtual functions. The v-table for a registered class can be found via a variable template static_vptr, nested in class default_registry. Classes used in virtual_ parameters need not be polymorphic, in the C++ sense. This time we implement value as an open-method. Nodes don’t have any virtual functions anymore.

struct Node {
    boost::openmethod::vptr_type vptr;
    using registry = boost::openmethod::default_registry; // short alias

    friend auto boost_openmethod_vptr(const Node& node, registry*) {
        return node.vptr;
    }

    Node() {
        vptr = registry::static_vptr<Node>;
    }
};

struct Variable : Node {
    Variable(int value) : v(value) {
        vptr = boost::openmethod::default_registry::static_vptr<Variable>;
    }

    int v;
};

struct Plus : Node {
    Plus(const Node& left, const Node& right) : left(left), right(right) {
        vptr = boost::openmethod::default_registry::static_vptr<Plus>;
    }

    const Node& left;
    const Node& right;
};

struct Times : Node {
    Times(const Node& left, const Node& right) : left(left), right(right) {
        vptr = boost::openmethod::default_registry::static_vptr<Times>;
    }

    const Node& left;
    const Node& right;
};

#include <boost/openmethod/initialize.hpp>

using boost::openmethod::virtual_;

BOOST_OPENMETHOD(value, (virtual_<const Node&> node), int);

BOOST_OPENMETHOD_OVERRIDE(value, (const Variable& var), int) {
    return var.v;
}

BOOST_OPENMETHOD_OVERRIDE(value, (const Plus& plus), int) {
    return value(plus.left) + value(plus.right);
}

BOOST_OPENMETHOD_OVERRIDE(value, (const Times& times), int) {
    return value(times.left) * value(times.right);
}

BOOST_OPENMETHOD_CLASSES(Node, Variable, Plus, Times);

auto main() -> int {
    boost::openmethod::initialize();
    Variable a{2}, b{3}, c{4};
    Plus d{a, b};
    Times e{d, c};
    std::cout << value(e) << "\n"; // 20
}

A call to value:

int call_value(const Node& node) {
    return value(node);
}

…​compiles to:

mov	rax, qword ptr [rdi]
mov	rcx, qword ptr [rip + value::fn+88]
jmp	qword ptr [rax + 8*rcx]         # TAILCALL

inplace_vptr

inplace_vptr_base and inplace_vptr_derived automate the creation and management of embedded vptrs. They are both CRTP mixin classes.

inplace_vptr_base is used at the root of a hierarchy. It defines a vptr member variable, and a boost_openmethod_vptr friend function that returns its value - just like in the previous example. inplace_vptr_derived is used in derived classes. Their constructors set the vptr member to point to v-table for the class.

WARNING

inplace_vptr_derived must be used at each level of the hierarchy, and reflect the actual inheritance relationships. Otherwise, the vptrs will not be set correctly, and the wrong overriders may be called.

struct Node : boost::openmethod::inplace_vptr_base<Node> {
};

struct Variable : Node, boost::openmethod::inplace_vptr_derived<Variable, Node> {
    Variable(int value) : v(value) {}

    int v;
};

struct Plus : Node, boost::openmethod::inplace_vptr_derived<Plus, Node> {
    Plus(const Node& left, const Node& right) : left(left), right(right) {}

    const Node& left;
    const Node& right;
};

struct Times : Node, boost::openmethod::inplace_vptr_derived<Times, Node> {
    Times(const Node& left, const Node& right) : left(left), right(right) {}

    const Node& left;
    const Node& right;
};

The rest of the program is as before, except that BOOST_OPENMETHOD_CLASSES needs not be called: inplace_vptr_base and inplace_vptr_derived take care of registering the classes and their inheritance relationships.

inplace_vptr_derived supports multiple inheritance: more than one base class can be listed after the class being defined.

The destructor of inplace_vptr_derived set the bases' vptrs back to the v-table for the bases, just like what C++ does for its native vptrs.

inplace_vptr_base and inplace_vptr_derived are aliased in namespace boost::openmethod::aliases.