Нет нужды описывать чем хорош 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;
Например вы пишете обертку для вызова 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
