Skip to content

A collection of C++ tools to have more fun with sum types (fast pattern matching, etc.)

License

Notifications You must be signed in to change notification settings

NativeInstruments/matchine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MATCHINE

Matchine is a small collection of C++ tools developed at Native Instruments for more fun with sum types, such variants, inheritance hierarchies, etc.

The three main tools are

  • ni::overload allows to create a single object that is a set of overloaded functions (free, lambdas, functors) following the C++ proposal P0051R2. Its main application is to construct visitors inplace for std::visit but it's not limited to that.

  • ni::match and ni::matcher allows to have a similar syntax of std::visit and ni::overload for open sum types, e.g. std::any, inheritance hierarchies, etc. It provides a simple pattern matching mechanism for types based on the signature of the provided functions.

  • ni::type_hierarchy allows to build inheritance based polymorphic types without using the slow RTTI mechanism. This improves the performance of casts by an order of magnitude.

Disclaimer. This documentation is work in progress, please check back soon for more details.

Installation

The library is header only. So simply clone and go. A CMakeLists.txt is provided.

Quick Introduction

ni::overload

#include <ni/functional/overload.h>
#include <vector>
#include <variant>
#include <string>
#include <iostream>

struct MyType {};
using var_t = std::variant<double, int, MyType>;

int main()
{
    std::vector<var_t>  vars = { var_t{3.14}, var_t{1337}, var_t{MyType{}} };
    for (auto const& var : vars)
    {
        // instead of requiring to construct a custom function object type elsewhere
        // one is able to overload several lambdas bound into one object.
        auto visitor = ni::overload
        (   [](double x) { return "float: " + std::to_string(x); }
        ,   [](int n) { return "int: " + std::to_string(n); }
        ,   [](auto) { return std::string("<unknown type>"); }
        );
        std::cout << std::visit(visitor, var) << std::endl;
    }
}

This will print:

float: 3.140000
int: 1337
<unknown type>

ni::match

Assume we have a polymorphic (i.e. sum) type, e.g. user input events:

struct Event { virtual ~Event(){} };
struct MouseEvent : Event {
    int x, y;
    MouseEvent(int x_, int y_) : x{x_}, y{y_} {}
};
struct KeyEvent : Event {
    char key;
    KeyEvent(char k) : key{k} {}
};
struct MyEvent : Event {};

Further we have a sequence of these events here simulated by putting different events into a vector:

using namespace std;

vector<shared_ptr<Event>> events =
{   make_shared<MouseEvent>(13, 37)
,   make_shared<KeyEvent>('k')
,   make_shared<MyEvent>()
};

ni::match calls the correct lambda depending on the detected event type:

for (auto const& event : events)
{
    auto s = ni::match(*event)
    (   [](MouseEvent const& m) {
            return to_string(m.x) + ", " + to_string(m.y); }
    ,   [](KeyEvent const& k) { return to_string(k.key); }
    ,   ni::otherwise(string("<unknown type>"))
    );
    std::cout << s << std::endl;
}

This will print:

13, 37
107
<unknown type>

ni::type_hierarchy

ni::type_hierarchy allows to build inheritance based polymorphic types without using the slow RTTI mechanism. This improves the performance of casts by an order of magnitude.

Let's model user input events (key press, mouse move, etc.). A possible way to do this is to use inheritance and might look like this:

struct Event { virtual ~Event() = default; };

struct MouseEvent : Event {
    int x, y;
    MouseEvent(int x_, int y_) : x{x_}, y{y_} {}
}

struct MouseButtonDownEvent : MouseEvent {
    enum button_t { left, right };
    button_t button;
    MouseButtonDownEvent(int x_, int y_, button_t b)
    :   MouseEvent{x_, y_}
    ,   button{b}
    {}
}

// struct MouseButtonUpEvent : MouseEvent { ... }
// struct KeyPressEvent : Event { ... }

This approach has one major drawback: casting between the types uses RTTI, which is flexible, but due to that also slow because it has to handle many edge cases. If we limit ourselves to tree-like hierarchies the identification of types can be

struct EventBase { ... };   // place to put common state shared by all objects.

// get the root type from the hierarchy which serves as reference/pointer to base class.
using Event = ni::type_hierarchy::from_base<EventBase>;

// derive custom types, the first template argument must always be the derived type
// itself. This is required due to the missing reflections in C++ and is used to
// assign a unique id to the new type.
struct MouseEvent : ni::sub_type<MouseEvent, Event> {
    int x, y;
    MouseEvent(int x_, int y_) : x{x_}, y{y_} {}
};
struct KeyEvent : ni::sub_type<KeyEvent, Event> {
    // ...
};

// 2nd level event
struct MouseButtonDownEvent : ni::sub_type<MouseButtonDownEvent, MouseEvent> {
    // ...
};

// etc..

See benchmarks for a speed comparison.

About

A collection of C++ tools to have more fun with sum types (fast pattern matching, etc.)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published