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.