Hello World
Consider the following program, intended to demonstrate the basics of virtual functions:
#include <iostream>
#include <memory>
struct Animal {
Animal(std::string name) : name(name) {}
virtual ~Animal() = default;
virtual void poke(std::ostream&) = 0;
std::string name;
};
struct Cat : Animal {
using Animal::Animal;
void poke(std::ostream& os) override {
os << name << " hisses";
}
};
struct Dog : Animal {
using Animal::Animal;
void poke(std::ostream& os) override {
os << name << " barks";
}
};
struct Bulldog : Dog {
using Dog::Dog;
void poke(std::ostream& os) override {
Dog::poke(os);
os << " and bites back";
}
};
auto main() -> int {
std::unique_ptr<Animal> a(new Cat("Felix"));
std::unique_ptr<Animal> b(new Dog("Snoopy"));
std::unique_ptr<Animal> c(new Bulldog("Hector"));
a->poke(std::cout); // prints "Felix hisses"
std::cout << ".\n";
b->poke(std::cout); // prints "Snoopy barks"
std::cout << ".\n";
c->poke(std::cout); // prints "Hector barks and bites back"
std::cout << ".\n";
return 0;
}
We are going to rewrite this using open-methods.
First we remove the poke
virtual functions from the domain classes:
#include <string>
struct Animal {
Animal(std::string name) : name(name) {}
std::string name;
virtual ~Animal() = default;
};
struct Cat : Animal {
using Animal::Animal;
};
struct Dog : Animal {
using Animal::Animal;
};
struct Bulldog : Dog {
using Dog::Dog;
};
Note that the Animal classes do not depend on iostreams anymore. This is a major advantage of open-methods over virtual functions: they make it possible to better organize dependencies.
Let’s implement poke
. First we need to include the library’s main header. It
defines a few macros, and injects a name - virtual_ptr
- in the global
namespace.
#include <iostream>
#include <boost/openmethod.hpp>
using boost::openmethod::virtual_ptr;
BOOST_OPENMETHOD(
poke, // method name
(std::ostream&, virtual_ptr<Animal>), // method signature
void); // return type
This defines a free function called poke
, which takes two arguments. The first
is the ostream
. The second argument corresponds to the implicit this
pointer
in a virtual function. It is now an explicit argument. Just like with virtual
functions, the exact function to execute is selected on the basis of the
argument’s dynamic type.
Unlike virtual functions, there is no such thing as a pure open-method that would make a class abstract. It is not possible to determine if an overrider is available from looking at just the current translation unit.
Let’s add overriders for Cat
and Dog
:
BOOST_OPENMETHOD_OVERRIDE(
poke, // method name
(std::ostream & os, virtual_ptr<Cat> cat), // overrider signature
void) { // return type
os << cat->name << " hisses"; // overrider body
}
BOOST_OPENMETHOD_OVERRIDE(poke, (std::ostream & os, virtual_ptr<Dog> dog), void) {
os << dog->name << " barks";
}
Bulldog::poke
calls the poke
it overrides in its Dog
base. The equivalent
for open-methods is next
, a function that is available only inside the body of
an overrider. It calls the next most specific overrider, i.e. what would have
been called if the overrider did not exist.
BOOST_OPENMETHOD_OVERRIDE(
poke, (std::ostream & os, virtual_ptr<Bulldog> dog), void) {
next(os, dog); // call base overrider
os << " and bites back";
}
All classes involved in open-method calls need to be registered using the
BOOST_OPENMETHOD_CLASSES
macro:
BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog, Bulldog);
Classes can be registered incrementally, as long as all the direct bases of a
class are listed with it in some call(s) to BOOST_OPENMETHOD_CLASSES
. For
example, Bulldog
can be added in a second call, as long as Dog
is listed as
well:
// in animals.cpp
BOOST_OPENMETHOD_CLASSES(Animal, Cat, Dog);
// in bulldog.cpp
BOOST_OPENMETHOD_CLASSES(Dog, Bulldog);
boost::openmethod::initialize();
must be called before any open-method call.
It builds the dispatch tables. Typically this is done in main
:
#include <boost/openmethod/compiler.hpp>
// only needed in the file that calls boost::openmethod::initialize()
auto main() -> int {
boost::openmethod::initialize();
// ...
}
We call poke
like any ordinary function. We can pass it the animals by
reference, because virtual_ptr
has a conversion constructor for that:
std::unique_ptr<Animal> felix(new Cat("Felix"));
std::unique_ptr<Animal> snoopy(new Dog("Snoopy"));
std::unique_ptr<Animal> hector(new Bulldog("Hector"));
poke(std::cout, *felix); // Felix hisses
std::cout << ".\n";
poke(std::cout, *snoopy); // Snoopy barks
std::cout << ".\n";
poke(std::cout, *hector); // Hector barks and bites
std::cout << ".\n";