An open-method is a free-standing function that has one or more virtual parameters. When it is called, it forwards to an overrider selected from a set by examining the dynamic types of the virtual parameters.
If this sounds like a virtual function, that’s because an open-method with one virtual parameter is equivalent to a virtual function - with one big difference: it exists outside of classes.
A virtual parameter is in the form virtual_ptr<Class>. It is a pointer-like
class that points to an instance of Class. virtual_ptr is defined in the library’s main namespace,
boost::openmethod.
To create an open-method that implements the postfix operation, we use the
BOOST_OPENMETHOD macro:
BOOST_OPENMETHOD(
postfix, (virtual_ptr<const Node> node, std::ostream& os), void);
postfix is the method’s name. It takes one virtual parameter, node, and one
non-virtual parameter, os. It returns void. The macro generates the
following function:
inline auto postfix(virtual_ptr<const Node> node, std::ostream& os) -> void {
// examine the type of *node
// select an overrider
// call it
}
Before we can call the method, we need to define overriders. For that we use the BOOST_OPENMETHOD_OVERRIDE macro:
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Variable> var, std::ostream& os), void) {
os << var->v;
}
The overrider must have virtual parameters in the same positions as in the method. The classes in the method’s virtual parameters must be accessible, non-ambiguous bases of the classes in the overrider. Note that this rules out repeated inheritance. Apart from this restriction, multiple and virtual inheritance are supported.
The non-virtual parameters must have exactly the same types as in the method.
Let’s look at another overrider:
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Plus> plus, std::ostream& os), void) {
postfix(plus->left, os);
os << ' ';
postfix(plus->right, os);
os << " +";
}
This one calls postfix recursively to print the left and right
sub-expressions. Note that we call the method just like an ordinary function.
postfix expects a virtual_ptr<const Node>, and we are passing it a plain
reference to a Node object. This works because virtual_ptr has conversion
constructors from plain references or pointers to objects, or from other
virtual_ptrs.
There are two more things we need to do.
OpenMethod is a library, not a compiler. It needs to be informed of all the classes that may be used as virtual parameters, and in method calls, and their inheritance relationships. We provide that information with the BOOST_OPENMETHOD_CLASSES macro:
BOOST_OPENMETHOD_CLASSES(Node, Variable, Plus, Times);
Classes can be registered multiple times, in any order, and incrementally. Every
direct base of a class must appear together with it in at least one call to
BOOST_OPENMETHOD_CLASSES. This enables the library to deduce the complete
inheritance lattice.
|
With a compiler that supports C++26 reflection, the class list is unnecessary. BOOST_OPENMETHOD_REGISTER_CLASSES finds the classes on its own - see Registering Classes by Reflection. |
The constructs used in this example require the classes to be polymorphic, in the standard C++ sense, i.e. they must have at least one virtual function. The library can also be used with non-polymorphic classes, with some restrictions.
Finally, we need to call boost::openmethod::initialize() before the first call
to an open-method. This builds the dispatch tables used during method calls. It
is typically done once, at the very beginning of main, and requires the
inclusion of <boost/openmethod/initialize.hpp>.
#include <boost/openmethod/initialize.hpp>
int main() {
boost::openmethod::initialize();
// ...
}
Putting it all together:
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;
};
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;
};
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// library code
// =============================================================================
// application code
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>
#include <iostream>
using boost::openmethod::virtual_ptr;
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;
}
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Plus> plus, std::ostream& os), void) {
postfix(plus->left, os);
os << ' ';
postfix(plus->right, os);
os << " +";
}
BOOST_OPENMETHOD_OVERRIDE(
postfix, (virtual_ptr<const Times> times, std::ostream& os), void) {
postfix(times->left, os);
os << ' ';
postfix(times->right, os);
os << " *";
}
BOOST_OPENMETHOD_CLASSES(Node, Variable, Plus, Times);
int main() {
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
}
Registering Classes by Reflection
When the compiler supports C++26 reflection (P2996), the library can work out the class list for itself. One call to BOOST_OPENMETHOD_REGISTER_CLASSES replaces every BOOST_OPENMETHOD_CLASSES in the file:
struct Animal { virtual ~Animal() = default; };
struct Cat : Animal {};
struct Dog : Animal {};
struct Bulldog : Dog {};
BOOST_OPENMETHOD(poke, (std::ostream&, virtual_<Animal&>), void);
BOOST_OPENMETHOD_OVERRIDE(poke, (std::ostream& os, Dog&), void) {
os << "bark";
}
BOOST_OPENMETHOD_REGISTER_CLASSES(); // registers all four classes
The macro scans the namespaces it is given - and the namespaces nested in them
- for the methods of the registry. It collects the classes those methods
dispatch on, then registers them, along with every class in the scanned
namespaces that derives from one of them. Bulldog above has no overrider of
its own and is named nowhere, and is registered all the same.
The arguments are groups of reflections, each optional, in this order: the
namespaces to scan; the classes to register; and the registries to register the
classes in - default_registry when none is named. A group holds one kind
of reflection, and is written in braces - or, for a group of one, as the
reflection itself:
BOOST_OPENMETHOD_REGISTER_CLASSES({^^zoo, ^^pets}, {^^my_registry});
BOOST_OPENMETHOD_REGISTER_CLASSES(^^zoo, ^^my_registry);
With no namespace group - BOOST_OPENMETHOD_REGISTER_CLASSES(), or any other
group on its own, {^^my_registry} included - the global namespace is scanned.
Name a namespace to scan only that one.
A listed class is registered whether a method dispatches on it or not, and it is one more root for the scan: every class the scan finds deriving from it is registered too, with the inheritance relations read from reflection.
A base class that no method dispatches on, and that is not listed, is not registered - it could never be selected on, and registering it would cost a lattice node, a hash slot and dispatch table space for nothing. So a hierarchy rooted in some general-purpose base contributes only the part of itself that takes part in dispatch. As soon as another method does dispatch on that base, it is registered, and the inheritance edges through it with it.
The namespaces decide where the derived classes are looked for, not which bases are recorded. A class between a registered class and a root is registered too, wherever it is declared - in a namespace the scan does not enter, or as a specialization of a class template, which the scan cannot find on its own - so that the inheritance lattice is complete. And each class is recorded with every registered class above it, so that a registration stands on its own, whatever another one - in another translation unit, over other namespaces - registers alongside it.
A scan does not descend into the std and boost namespaces, so scanning the
global namespace costs little more than a narrower one would - a method cannot
dispatch on a class the program never heard of anyway. The exclusion applies to
recursion only: a namespace listed explicitly is always scanned, which is how
a class in std or boost gets registered.
The scan runs when the registrar the macro declares is instantiated, which current compilers do at the end of the translation unit: it sees the whole file, wherever the macro sits. The standard promises less. A class or a method the scan would select, declared after the macro in the same translation unit, makes the program ill-formed, no diagnostic required. So put the macro after the declarations it is meant to find: at the bottom of the file.
Virtual and multiple inheritance are supported. Unlike
BOOST_OPENMETHOD_CLASSES, which rejects it, repeated inheritance is not an
error here: a class is recorded under the bases it reaches unambiguously
through public inheritance, and a base it inherits more than once - which no
reference to it can be converted to - is left out of its list.
Without reflection - in C17, or in C26 without the compiler flag that enables
it - the macro expands to nothing. A file that also calls
BOOST_OPENMETHOD_CLASSES therefore builds under either standard.
What Reflection Cannot Find
A method is found through any declaration that names its method type: the
alias BOOST_OPENMETHOD declares alongside the method, a using declaration of
your own, or any of the method’s registrar objects. None of those depends on the
method having an overrider, so a method declared with BOOST_OPENMETHOD is
always found. A method’s return type is not a root: a covariant return type is
registered like any other class, when it derives from a root, or when it is
listed.
Four situations remain outside the scan’s reach, and need to be registered by
other means - listed in the macro, or given a BOOST_OPENMETHOD_CLASSES of
their own:
-
a class in a namespace the macro does not scan, unless it sits between a class the scan finds and a root;
-
a specialization of a class template that no method dispatches on, and that no class the scan finds derives from - one used only as an overrider parameter, say. Specializations are not members of a namespace, so the scan does not see them; list it:
BOOST_OPENMETHOD_REGISTER_CLASSES({^^Pet<int>}); -
a core API method whose
method<…>type is spelled out in full at every use, with nousingdeclaration of its own and no overrider - nothing names it; -
a program that declares no method at all, and uses
virtual_ptron its own - there is no virtual parameter for the scan to start from.