Pull to refresh

C++ pattern matching

Reading time 7 min
Views 31K

Нет нужды описывать чем хорош pattern matching. Так как в любом случае такой конструкции в С++ нет.
Без него же работа с шаблонами, часто обрастает лесами понятного и полезного кода.
Итак предлагаю способ некоего подобия pattern matching`а для С++14 (скорее даже type matching'a), который укладывается в 50 строк кода, не использует макросы и вообще кросс-компиляторный.


Сначала пример использования: http://coliru.stacked-crooked.com/a/6066e8c3d87e31eb


template<class T>
decltype(auto) test(T& value) {
    return match(value
        ,[](std::string value)    { cout << "This is string"; return value + " Hi!"; }
        ,[](int i)                { cout << "This is int";    return i * 100; }
        ,[](auto a)               { cout << "This is default";return nullptr; }
    );
}

compile-time Условия: http://coliru.stacked-crooked.com/a/ccb13547b04ce6ad


match(true_type{}
         ,[](bool_constant< T::value == 10 >)                        { cout << "1" ; }
         ,[](bool_constant< (T::value == 20 && sizeof...(Args)>4) >) { cout << "2" ; }
    );

Возвращаем тип: http://coliru.stacked-crooked.com/a/0a8788d026008b4b


auto t = match(true_type{}
           ,[](is_same_t<T, int>) -> type_holder<short>  { return{}; }
           ,[](auto)              -> type_holder<T>      { return{}; }
         );

using I = typename decltype(t)::type;             
I i = 1000000;

Если вы вообще не понимаете зачем нужен pattern matching (в частности по типу)

Например вы пишете обертку для вызова java функции из С++ (через jni).
Обычно это бы выглядело:


int call_java_helper(int element){
   return  jni->CallIntMethod(....);
}

float call_java_helper(float element){
   return  jni->CallFloatMethod(....);
}

void call_java_helper(nullptr_t){
    jni->CallVoidMethod(....);
}

template<class T>
auto call_java(T element){
     cout << "Start Java Call";
     return call_java_helper(element);
}

C использованием pattern matching:


template<class T>
auto call_java(T element){
     cout << "Start Java Call";
     return match(elment, 
         ,[](int element)  { return jni->CallIntMethod(element); }
         ,[](float element){ return jni->CallFloatMethod(element); }
         ,[](auto)         { jni->CallVoidMethod(); }
     );
}

Всё собрано и в одном месте.


Думаю можно провести некую аналогию с Rust pattern matching по enum'ам


Синтаксис


match(value                    // <- значение, тип которого сравнивается
     ,[](std::string value)    { /* будет сравниваться с std::string */ }         
     ,[](int i)                { /* можно возвращать значения различных типов  */ return i+100; } 
     ,[](auto a)               { /* Аналог default: в switch */ }     
); 

Принцип работы


Основная логика:


namespace details {
    template<class T, class Case, class ...OtherCases>
    decltype(auto) match_call(const Case& _case, T&& value, std::true_type, const OtherCases&...) {
        return _case(std::forward<T>(value));
    }

    template<class T, class Case, class ...OtherCases>
    decltype(auto) match_call(const Case& _case, T&& value, std::false_type, const OtherCases&...) {
        return match(std::forward<T>(value), other...);
    }
}

template<class T, class Case, class ...Cases>
decltype(auto) match(T&& value, const Case& _case, const Cases&... cases) {
    using namespace std;
    using args = typename FunctionArgs<Case>::args;               // <- Это самое интересное место!
    using arg = tuple_element_t<0, args>;
    using match = is_same<decay_t<arg>, decay_t<T>>;
    return details::match_call(_case, std::forward<T>(value), match{}, cases...);
}

// это для default
template<class T, class Case>
decltype(auto) match(T&& value, const Case& _case) {
    return _case(std::forward<T>(value));
}

Функция match принимает на вход сравниваемое значение value и список лямбд (которые служат case'ми). У каждой лямбды должен быть ровно один аргумент. С помощью FunctionArgs мы определяем тип этого аргумента. Затем проходим по всем лямбдам и вызываем ту у которой совпадает тип аргумента.


Предполагается что последняя лямбда может содержать generic аргумент. Поэтому тип её аргументов не проверяется. Она просто вызывается. Если она не generic, и тип не совпадает компилятор просто выдаст ошибку (правда предварительно попытается привести к типу).


Можно было бы как то определять generic последняя лямбда или нет, но как это сделать — неизвестно.


FunctionArgs — модифицированная версия http://stackoverflow.com/a/27867127/1559666 :


template <typename T>
struct FunctionArgs : FunctionArgs<decltype(&T::operator())> {};

template <typename R, typename... Args>
struct FunctionArgsBase{
    using args  = std::tuple<Args...>;
    using arity = std::integral_constant<unsigned, sizeof...(Args)>;
    using result = R;
};

template <typename R, typename... Args>
struct FunctionArgs<R(*)(Args...)> : FunctionArgsBase<R, Args...> {};
template <typename R, typename C, typename... Args>
struct FunctionArgs<R(C::*)(Args...)> : FunctionArgsBase<R, Args...> {};
template <typename R, typename C, typename... Args>
struct FunctionArgs<R(C::*)(Args...) const> : FunctionArgsBase<R, Args...> {};

P.S.


Должен заметить, что существует также https://github.com/solodon4/Mach7, которая также реализует pattern matching (можно даже сказать что в более полной мере). Но синтаксис, обилие макросов, её объём, и то что на момент написания статьи она находилась в несколько… разобранном состоянии оттолкнули автора в сторону этого велосипеда…
Впрочем, будем надеяться на светлое будущее в лице с++23 а может и с++20 с поддержкой pattern matching'a со стороны языка.


Весь код текстом (для копи-паста)
/*
std::string s = "12";
cout << match(s
    ,[](int& i) { return "int"; }
    ,[](bool& b) { return "bool"; }
    ,[](std::string& s) -> auto& { s += " GAV"; return s; }
    ,[](auto j) { cout << "default one"; return j; }
);
*/

#include <tuple>

template <typename T>
struct FunctionArgs : FunctionArgs<decltype(&T::operator())> {};

template <typename R, typename... Args>
struct FunctionArgsBase{
    using args  = std::tuple<Args...>;
    using arity = std::integral_constant<unsigned, sizeof...(Args)>;
    using result = R;
};

template <typename R, typename... Args>
struct FunctionArgs<R(*)(Args...)> : FunctionArgsBase<R, Args...> {};
template <typename R, typename C, typename... Args>
struct FunctionArgs<R(C::*)(Args...)> : FunctionArgsBase<R, Args...> {};
template <typename R, typename C, typename... Args>
struct FunctionArgs<R(C::*)(Args...) const> : FunctionArgsBase<R, Args...> {};

// forward declarations
template<class T, class Case, class ...Cases>
decltype(auto) match(T&& value, const Case& _case, const Cases&... cases);
template<class T, class Case>
decltype(auto) match(T&& value, const Case& _case);

namespace details {
    template<class T, class Case, class ...OtherCases>
    decltype(auto) match_call(const Case& _case, T&& value, std::true_type, const OtherCases&... other) {
        return _case(std::forward<T>(value));
    }

    template<class T, class Case, class ...OtherCases>
    decltype(auto) match_call(const Case& _case, T&& value, std::false_type, const OtherCases&... other) {
        return match(std::forward<T>(value), other...);
    }
}

template<class T, class Case, class ...Cases>
decltype(auto) match(T&& value, const Case& _case, const Cases&... cases) {
    using namespace std;
    using args = typename FunctionArgs<Case>::args;
    using arg = tuple_element_t<0, args>;
    using match = is_same<decay_t<arg>, decay_t<T>>;
    return details::match_call(_case, std::forward<T>(value), match{}, cases...);
}

// the last one is default
template<class T, class Case>
decltype(auto) match(T&& value, const Case& _case) {
    return _case(std::forward<T>(value));
}



Update


В комментариях предложили на мой взгляд более совершенный способ https://habrahabr.ru/post/282630/#comment_8873766.
С его помощью можно делать сопоставление с образцом сразу по нескольким значениям.
Даже если вы делаете сопоставление всего по одному значению, вам может понадобится просто передать дополнительные аргументы в функцию. Например в следующем примере в clang необходимо передавать в функцию тип(gcc и VS работают и без этого):


    template<class Out, class ...Args>
    inline Out run(Args&&...args){
        auto in = std::tie(std::forward<Args>(args)...);
        return match(type_holder<Out>()
                ,[&](type_holder<void>){ command(Parcel(in), Parcel()); }
                ,[&](auto type)->Out{
                    Out out;    // clang выдаст ошибку : переменная не может быть типа void . 
                    //typename decltype(type)::type out;    // А так работает
                    command(Parcel(in), Parcel(out));
                    return out;
                }
        );
    }

Но вот что-то в духе:


match(
   [&](false_type, auto)      { command(); },
   [&](true_type,  auto type) {
         typename decltype(type)::type out;
         command(Parcel(out));    
   }
)(is_void{}, type_holder<Out>{});

Сделать невозможно.


В случае не нахождения "образца" компилятор выдаёт вполне внятные сообщения.


http://coliru.stacked-crooked.com/a/1f3723d422ef05ee


Код
namespace detail {
    template <class ...> struct match;

    template <class Head, class ... Tail>
    struct match<Head, Tail...> : match<Tail...>, Head {
        template <class Head2, class ... Tail2>
        match(Head2&& head, Tail2&& ... tail) : match<Tail...>(std::forward<Tail2>(tail)...), Head(std::forward<Head2>(head)) {}
        using Head::operator();
        using match<Tail...>::operator();
    };

    template <class T>
    struct match<T> : T {
        template <class R>
        match(R&& r) : T(std::forward<R>(r)) {}
        using T::operator();
    };
}

template <class ... Cases>
auto match(Cases&& ... cases) {
    return detail::match<typename std::decay<Cases>::type...>{std::forward<Cases>(cases)...};
}

int main() {
    int io = 100;
    int i = 1000;
    match(
        [](int i1 , auto i2) { cout << "int" << i1 << " " << i2; }
        ,[](short, char)     { cout << "short"; }
        ,[&](auto...)        { cout << "auto " << io; }
    )(1,  i);
    return 0;
}

P.S. Сколько я не бился, не смог синтаксис привести к виду match(1,2) ( cases...) так чтобы move конструктор не вызывался http://coliru.stacked-crooked.com/a/70d1aec24c26642a

Tags:
Hubs:
+33
Comments 48
Comments Comments 48

Articles