yomm2

Using YOMM2 without macros

YOMM2 provides a public interface that does not require using macros. This can be useful in certain situations, for example when combining open methods and templates - see the templates tutorial.

The following code is a partial rewrite of the synopsis example that does not use any macros.

#include <yorel/yomm2/core.hpp>

using namespace yorel::yomm2;

class Animal {
  public:
    virtual ~Animal() {
    }
};

class Dog : public Animal {};
class Bulldog : public Dog {};

use_classes<Animal, Dog, Bulldog> use_animal_classes;

The new use_classes template takes any number of classes, and infers the inheritance relationships they may have between them. Instantiating a use_classes object registers the classes, in the same fashion as - but more conveniently than - a series of invocations of register_class.

struct kick_key;
using kick_method = method<kick_key, std::string(virtual_<Animal&>)>;

A YOMM2 method is implemented as a singleton of an instance of the method template. The second argument is obviously the signature of the method - including the return type and the virtual_ markers.

What about the first argument? Its role is to separate different methods with the same signature. Consider a more animal-friendly method:

struct feed_key;
using feed_method = method<feed_key, std::string(virtual_<Animal&>)>;

In the absence of the first parameter, kick and feed would be the same method. Together, the two arguments provide a unique key for the method. Since the kick_key and feed_key types are local to the current namespace, this scheme also protects against accidental interference across namespaces.

The same key can be used for more than one method, provided that the signatures are different. The good practice is to use the same key for all the methods in a namespace that have the same name.

Now let’s add a definition to the method:

std::string kick_dog(Dog& dog) {
    return "bark";
}

kick_method::add_function<kick_dog> add_kick_dog;

Note that the name of the function serving as a method definition must be unique; in presence of overloads, we would have no means of picking the appropriate function. Function templates and explicit specialization can also be used for this purpose.

What about next? The constructor of add_function can be passed a pointer to a function that will be set to the function’s next definition by update. The pointer type is available in the method as next_type.

kick_method::next_type kick_bulldog_next;

std::string kick_bulldog(Bulldog& dog) {
    return kick_bulldog_next(dog) + " and bite back";
}

kick_method::add_function<kick_bulldog> add_kick_bulldog(&kick_bulldog_next);

We can now call the method. The class contains a static function object named fn, whose operator() has the signature specified in the method declaration, minus the virtual_<> decorators.

BOOST_AUTO_TEST_CASE(test_synopsis_functions_no_macros) {
    update();

    std::unique_ptr<Animal> snoopy = std::make_unique<Dog>();
    BOOST_TEST(kick_method::fn(*snoopy) == "bark");

    std::unique_ptr<Animal> hector = std::make_unique<Bulldog>();
    BOOST_TEST(kick_method::fn(*hector) == "bark and bite back");
}

A peek inside the two main YOMM2 macros

The code in the example above is essentially what YOMM2_DECLARE/declare_method and YOMM2_DEFINE/define_method generate.

In addition, declare_method generates an ordinary inline function that forwards to the fn object nested inside the method. Importantly, ordinary functions can be overloaded, and their address can be taken, which is not the case for function objects.

declare_method also declares a guide function that enables define_method to find the method being specialized.

define_method wraps the function body inside a class, along with a next static variable. It fakes a call to a guide function named after the method, passing it declval arguments for the definition’s parameter list. The compiler performs overload resolution, and the macro uses decltype to extract the result type, i.e the method’s class, and registers the definition and the next pointer with add_function.

In the process, both macros need to create identifiers for the various static objects, and the name of the function inside the definition wrapper class. These symbols are generated by two macros; in both cases, the symbols are copiously obfuscated, to minimize the risk of collision with the user’s symbols. YOMM2 provides helper macros for this:

In addition, the header provides

These macros are defined by header file yorel/yomm2/symbols.hpp.

Trimming verbosity

The “synopsis” example is quite verbose. Many of the names used in it are pure noise. They define static objects, for the sole purpose of executing their constructor. They are never referenced explicitly afterwards. In a language like Python, we would simply call functions at file scope; C++ does not allow that, or only by ruse and abuse.

Let’s rewrite the example, this time using the symbol-generation macros, and a helper.

(Animal classes same as before)

#include <yorel/yomm2/core.hpp>
#include <yorel/yomm2/symbols.hpp>

using namespace yorel::yomm2;

YOMM2_STATIC(use_classes<Animal, Dog, Bulldog>);

struct YOMM2_SYMBOL(kick);

using kick_method = method<YOMM2_SYMBOL(kick), std::string(virtual_<Animal&>)>;

add_function is a workhorse that is intended to be used directly only by define_method. YOMM2 has another mechanism that is a bit more high level: definition containers.

A definition container is a struct that, at the minimum, contains a static function named fn. Containers are added to methods via the add_definition nested type:

struct kick_dog {
    static std::string fn(Dog& dog) {
        return "bark";
    }
};

YOMM2_STATIC(kick_method::add_definition<kick_dog>);

This may not seem like a huge improvement, until we need a next function. If the container has a static member variable called next, and it is of the appropriate type, add_definition will pick it up for update to fill. Static member variables are a bit clumsy, because, unlike functions, they must be declared inside the class, and defined outside. Methods have a nested CRTP helper to inject a next into a container.

struct kick_bulldog : kick_method::next<kick_bulldog> {
    static std::string fn(Bulldog& dog) {
        return next(dog) + " and bite back";
    }
};

YOMM2_STATIC(kick_method::add_definition<kick_bulldog>);

Do you have doubts about the value of definition containers? Here are two more reasons why you should use them.

  1. Containers are the core of the best pattern I could come up with to implement templatized methods and definitions.

  2. In the future, additional functionality may be added to containers.

The rest of the example is as before.