OpenMethod provides a macro-free interface: the core API. This is useful in certain situations, for example when combining open-methods and templates.

Let’s rewrite the postfix method using the core API. An open-method is implemented as an instance of the method class template. Its parameters are an identifier type and a function type:

#include <boost/openmethod/core.hpp>

struct postfix_method_id;

using postfix = method<
    postfix_method_id, void(virtual_ptr<const Node> node, std::ostream& os)>;

The postfix_method_id class acts as the method’s identifier: it separates it from other methods with the same signature. For example, we could have another method, prefix, which writes expressions in prefix notation. It would have the same signature. Without the identifier argument, prefix and postfix would be the same method.

The exact name of the identifier class does not matter. The class needs not be defined, only declared.

Inventing identifier class names can get tedious, so OpenMethod provides a macro for that: BOOST_OPENMETHOD_ID. Let’s use it:

#include <boost/openmethod/macros.hpp>

struct BOOST_OPENMETHOD_ID(postfix);

using postfix = method<
    BOOST_OPENMETHOD_ID(postfix),
    void(virtual_ptr<const Node> node, std::ostream& os)>;

We said macro-free interface, but here is a macro again! Well, we are not forced to use the macro. There is a benefit though: it is used in the implementation of high-level macros like BOOST_OPENMETHOD. This makes it possible to mix the two styles, for example to define a method using the macro, and add overriders using the core API.

We call the method via the nested function object fn:

postfix::fn(node, std::cout);

Overriders are ordinary functions. We add them to the method via the class template override, nested in the method class. Its constructor adds the functions passed as template arguments to the method.

auto postfix_variable(virtual_ptr<const Variable> node, std::ostream& os) {
    os << node->v;
}

static postfix::override<postfix_variable> override_postfix_variable;

Once again we find ourselves inventing a name for a single use. In C++26, we will probably have the Python-like _ for this. In the meatime, we can use another convenience macro: BOOST_OPENMETHOD_REGISTER. It takes a class, and instantiates a static object with an obfuscated name:

#include <boost/openmethod/macros.hpp>

auto postfix_plus(virtual_ptr<const Plus> node, std::ostream& os) {
    postfix::fn(node->left, os);
    os << ' ';
    postfix::fn(node->right, os);
    os << " +";
}

auto postfix_times(virtual_ptr<const Times> node, std::ostream& os) {
    postfix::fn(node->left, os);
    os << ' ';
    postfix::fn(node->right, os);
    os << " *";
}

BOOST_OPENMETHOD_REGISTER(postfix::override<postfix_plus, postfix_times>);

Again, use of this macro is completely optional.

We register the classes with use_classes:

BOOST_OPENMETHOD_REGISTER(use_classes<Node, Variable, Plus, Times>);

Finally, we call the method via the static member of the method class fn:

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

Methods and Templates

The primary purpose of the core API is to make open-methods inter-operate with templates.

Plus and Times are obvious candidates for templatization. They only differ by the operation they perform, and we already have templates for that in the standard library:

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

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

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

template<typename Op>
struct BinaryOp : Node {
    BinaryOp(const Node& left, const Node& right) : left(left), right(right) {}
    int value() const override { return Op()(left.value(), right.value()); }
    const Node& left;
    const Node& right;
};

Let’s go back to defining postfix using BOOST_OPENMETHOD. It is more convenient, as it generates postfix as an ordinary inline function, which can be overloaded, passed around by address, etc. The overrider for Variable does not involve templates, so we can write it as before:

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

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

We write a function template that renders binary operations in postfix notation:

template<class BinaryOp, char Op>
auto postfix_binary(virtual_ptr<const BinaryOp> node, std::ostream& os) {
    postfix(node->left, os);
    os << ' ';
    postfix(node->right, os);
    os << " " << Op;
}

Macro BOOST_OPENMETHOD_TYPE takes the same parameters as BOOST_OPENMETHOD, and expands to the core method instance. That is how we access its nested overrider class template:

BOOST_OPENMETHOD_TYPE(
    postfix, (virtual_ptr<const Node> node, std::ostream& os), void)::
    override<
        postfix_binary<BinaryOp<std::plus<int>>, '+'>,
        postfix_binary<
            BinaryOp<std::multiplies<int>>, '*'>> add_binary_overriders;

We can register the classes with BOOST_OPENMETHOD_CLASSES, and write main like so:

BOOST_OPENMETHOD_CLASSES(
    Node, Variable, BinaryOp<std::plus<int>>, BinaryOp<std::multiplies<int>>);

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

This example only scratches the surface of what can be done by combining templates, the core API, and libraries such as Boost.Mp11.