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