Pull to refresh

Comments 48

Выглядит круто. А как по скорости по сравнению с работой через if'ы? Или оно compile-time?
Это compile-time. Тут плюсы скорее даже не в скорости, а в том что if'ми это сделать невозможно. Попробуйте boost::variant if'ми разобрать.
Я не самый специалист в этом деле, но компилятор действительно в данном случае может оптимизацией выкинуть объект лямбды и заинлайнить код в ней? Для производительности это критическая штука. Если у кого-то есть возможность, пожалуйста, можете проверить или подсказать?

А идея крутая и лежит на поверхности. И как я сам не додумался…
Да, конечно компилятор такие вещи выкидывает. На этом Boost::Hana построена. Собственно описанный тут велосипед — это как раз её кусочек, только чуть менее удобный в использовании.

Можете почитать — выглядит почти так, как тут.
А чем менее удобная? Визуально в Hana писанины больше. + Я не пробовал, но почти уверен что если начать тянуть эту часть из Boost'a пара мегабайт (как минимум) кода обеспечена.
Тем, что вы должны прямо «здесь и сейчас» обработать тип. Передать его дальше для обработки нельзя, обработать список объектов разной природы — тоже. Такой себе маленький кусочек большой системы.

Вот как, например, у вас обработка списка элементов будет выглядеть? Банальный for_each?

А что касается «пары мегабайт» — за что конкретно мы боремся? Время компиляции по сравнению со многими другими библиотеками у Boost:Hana уменьшено, там на сайте графики есть — хотя, понятно, тут всегда важен баланс между скоростью и возможностями…
Тем, что вы должны прямо «здесь и сейчас» обработать тип. Передать его дальше для обработки нельзя...

Вы имеете ввиду что теряется информация о типе? Если да, то я не понимаю как.


Банальный for_each?

Извините, но я считаю что это разные вещи. Здесь нужен отдельный алгоритм прохода по кортежу, и он у меня есть. Так же как и Hana'вский он принимает на вход кортеж, и лямбду. В лямбде можно уже использовать pattern matching.
Более того — текущий pattern matching можно использовать в Hana::for_each как есть:


    hana::for_each(hana::make_tuple(0, '1', "234", 5.5), [&](auto x) {
         match(x
          ,[](std::string value)    { cout << "This is string";  }
          ,[](int i)                 { cout << "This is int";    }
          ,[](auto a)                  { cout << "This is default"; }
        );
    });

Hana — это библиотека. И мне она не нравится. Прежде всего по тому что там свои кортежи.
Здесь же описана конкретная конструкция. Мне кажется это как сравнивать STL и конкретно взятый алгоритм.

Здесь же описана конкретная конструкция. Мне кажется это как сравнивать STL и конкретно взятый алгоритм.
Если под «конкретно взятым алгоритмом» вы понимаете «что-то аналогичное алгоритму из STL'евского <algorithm>а», то да, конечно. Алгоритмы из STL'я удобны не потому, что они реализуют какую-то супермудрость, а потому, что они являются частью большой библиотеки, которые согласованы между собой.

То же самое и здесь: да, вы можете использовать ваш матчер с hana::make_tuple, но… зачем?

Насчёт что лучше — отдельные «алгоритмы» или целые библиотеки можно спорить, но хочу заметить, что даже ваш матчер не существует в вакууме, а тянет за собой FunctionArgs :-)

Практика показывает, что «отдельные маленькие хорошенькие штучки» хороши в теории, но на практике библиотеки — удобнее. До определённого предела, конечно (вряд ли кто захочет использовать библиотеку на терабайт, даже если она будет уметь очень-очень много всего-всего-всего), но в большинстве случаев — это так.

Hana не об этом. Hana позволяет заменить вырвиглазные и тормозные шаблоны Boost Fusion и MPL на читаемые и относительно быстрые constexpr вычисления. Такой минихаскель построенный на C++ constexpr, где вычисления идут в compile-time над типами и константами C++.


Заинлайниться же в C++ может почти все что угодно (кроме не tail рекурсии), даже при раздельной компиляции с LTO.


Паттерн матчинг бывает 2х типов: comile time и run time. Первый — это пример автора и hana (которая позволила бы написать еще короче и понятнее), а так же известный variant. Если же нужен полноценный run time pattern matching почти как в хаскеле (ака dynamic_cast на стероидах, да еще и быстрее), то Mach7 как раз об этом (но десять раз подумайте — может вам просто нужен хаскель?).


P.S. Я экспериментировал с Boost Hana — и она замечательна, планирую написать статьи о ней.

Элементарный пример — std::for_each. Переданное лямбда-выражение инлайнится.
Косвенная проверка:
https://godbolt.org/g/pNIa8n
С -O2 или -O3 вообще результат вычисляется в compile-time.

Пожалуйста, укажите лицензию на ваш код. Формально, использовать код без указанной лицензии нельзя, ибо по умолчанию лицензия на код проприетарная, т. е. использовать код можете только вы, где бы вы его при этом не публиковали.


Не обязательно копипастить весь текст лицензии, укажите хотя бы её название и ваш копирайт. Например:


/* The MIT License (MIT)
 * Copyright (c) 2016 tower120
 */

Также было бы очень здорово, если бы вы опубликовали код не только в статье на Хабре, но и, скажем, в виде GitHub Gist. Так вашему коду можно поставить звезду и потом не потерять его. А можно форкнуть и дополнить. И все форки будут в одном месте и тоже не потеряются.

Всегда было интересно, а как можно обнаружить кусок чьего-то кода в большом проекте, если ни авторства, ни лицензии не указано? И кто этим занимается — есть какие-то автоматизированные сервисы, или специально обученные люди? И чем может быть чревато использование такого кода? А если проект с закрытым исходным кодом?
1. Никому не интересно обнаруживать абстрактный «кусок чьего-то кода». Компаниям интересно обнаружить случайно залитый код с github'а без лецензий — для чего есть тулы типа той же FOSSology, а для владельцев авторских прав интересно отловить свой код — и уж они-то знают обычно, как это сделать.
2. Есть и специальные сервисы и люди — но обычно это делается тогда, когда с пользователя кода можно денег содрать. Поймать «дядушку Ляо» на использовании чужого кода можно, конечно — но дальше что? Даже затраты на адвокатов не окупите.
3. А чем чревата продажа пиратских фильмов или аудизаписей? Принцип тот же: до $100'000 за копию в теории, но на практике вряд ли больше нескольких центов. Гораздо более чувствительным может быть требование прекратить новые продажи до того момента пока чужой код не будет изъят — но, опять-таки, «дядушку Ляо» так не остановить.
4. Что значит «а если проект с закрытым исходным кодом»? А какая вообще разница? Суд не интересует какие там у вас в фирме политики. Незаконное копирование == штраф.
1,2,3. Ну в общих чертах я все это как-то так себе и представлял. Просто подумал, может есть есть какая-то еще специфика, о которой я не знаю, а все знают.
4. Это значит, что некая условная корпорация берет из интернета кусок кода, распространяемый, например, под GPL, или вообще без указания лицензии (а значит, по умолчанию она проприетарная), и засовывает его в свой проприетарный же продукт с закрытым кодом. А распространяет она бинарники. Кто и как узнает, что в скомпилированных бинарниках есть кусок чужого кода, вставленный туда без разрешения автора? И какими могут быть реальные последствия этого для корпорации? И как это вообще можно доказать?
Я однажды в книжке серии Библиотечки Квант про двойные прочитал шикарную фразу (цитирую по памяти): «Если вы звёзды имели спектр идеально чёрного тела, то их движение друг относительно друг друга было бы невозможно заметить. К счастью мир не столь совершенен, а потому познаваем.»

И вот этот вот «мир не столь совершенен, а потому познаваем» — очень часто играет в подобного рода вещах.

Кто и как узнает, что в скомпилированных бинарниках есть кусок чужого кода, вставленный туда без разрешения автора?
Можно отловить характерные участки кода, строки и т.п. Обычно автору эо сделать несложно: берётся место, которое «давно стоило бы переписать, да руки не доходят» и проверяется его наличие. Так как это место сделано «шероховато», то очень маловероятно что кто-то сделает его так же криво очень мала (все счастливые семьи счастливы одинаково, каждая несчастливая семья несчастлива по-своему).

И как это вообще можно доказать?
А как доказывают что Петя убил Васю ледорубом? Следы ищут, «отпечатки пальцев», экспертов привлекают и т.д. и т.п. Потом суд решает — достаточно ли улик. Достаточно должно быть только для того, чтобы заставить код показать экспертам (противной стороне, понятно, его показывать нельзя — секреты фирмы и прочее, но если вы отказываетесь его показать независимым экспертам, ссылаясь на «потерянные флоппи», то вам нужно будет после этого очень сильно постараться, чтобы судья поверил в вашу невиновность).

И какими могут быть реальные последствия этого для корпорации?
До сих пор самое страшное что случалось — запрещали продажу конкретных устройств. Что само по себе чревато. До «термоядерной опции» (указать что нарушение, скажем, GPL, лишает вас права на использование соответствуещего кода вообще, навсегда) никто пока не добирался.

Но это все про крупные проекты. Мелкие куски кода (типа 50 строк приведённых в статье) обычно проще выкинуть и переписать заново, чем разбираться с лицензиями. Когда SCO пыталась всех пользователей Linux засудить спорный кусок кода в Linux'е обнаружили, к примеру. На всякий случай его переписали — просто «на всякий случай»…
Можно покороче:
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();
};

template <> struct match<> {};

}


template <class T, class ... Cases>
decltype(auto) match(T&& value, Cases&& ... cases) {
	return detail::match<typename std::decay<Cases>::type...>{std::forward<Cases>(cases)...}(std::forward<T>(value));
}
Небольшая поправочка:
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 T, class ... Cases>
decltype(auto) match(T&& value, Cases&& ... cases) {
    return detail::match<typename std::decay<Cases>::type...>{std::forward<Cases>(cases)...}(std::forward<T>(value));
}

К сожалению, теперь я не могу понять что тут вообще происходит...

Из всех всех лямбд собирается одна «суперлямбда» в которой живут они все — вместе со всеми своими operator'ами. Дальше вызывается её operator() — и если нам повезло, то он, собственно, и срабатывает.

Если не повезло — не срабатывает.

Это не совсем эквивалент, так как перебора нет и нельзя передать много «уточняющих» лямб: если будет десколько лямбд, которые, теоретически, могут принять аргумент, то они все будут пытаться примениться и получится ошибка компиляции.

Вангую очень веселое сообщение об ошибке, если что-то не получилось.

Не понял пример использования. Зачем оно надо?
В основном для всяких JSON'ов и RPC. Когда у вас могут приходить всякие boost:any, а вам это как-то нужно в конкретные типы привератить. Ну или в обратном направлении.
Вы все еще парсите сетевые данные руками? Тогда мы не идем к вам… работать :)
Чтобы было удобно пользоваться алгебраическими типами данных.
Например, нужно получить из функции структуру, в которой нас интересует только одно поле. Пишем:

let MyStruct{field, ..} = my_function();
Или вместо

std::size_t found = str.find(str2);
if (found!=std::string::npos)
std::cout
А зачем такая штука может использоваться? Можно пример прикладного кода с использованием такой конструкции?
А зачем вообще нужно метапрограммирование — вы понимаете?

Это не издевательство, я реально не понимаю как можно практически использовать метапрограммирование и не понимать когда и как вам может пригодиться pattern matching. Я могу у нас в проекте показать сразу пяток мест, где бы это можно было с пользой применить если бы C++14 был разрешён. А так — приходится по старинке, с шаблонами с частичной специализацией…

Посмотрите на ваш любимый пример, использующий метапрограммирование с разными типами, и посмотрите сколько там ненужного boilerplate…
А так — приходится по старинке, с шаблонами с частичной специализацией


Так а чем это плохо?

Посмотрите на ваш любимый пример, использующий метапрограммирование с разными типами, и посмотрите сколько там ненужного boilerplate…


Что вы имеете в виду?
Так а чем это плохо?
Тем что на написание и чтение всего это кода уходит время, очевидно. Ваш К.О.

Так-то вообще всё, что можно сделать на C, C++, Haskell'е или там Java можно сделать прямо в машинных кодах… в конце-то-концов ничего больше CPU исполнять не умеет… вопрос только в том, сколько у вас на это времени уйдёт.

Что вы имеете в виду?
Возьмите любой пример и посмотрите — сколько вам придётся завести лишних trait'ов, шаблонов и прочего, чтобы сделать хоть что-нибудь.

Возьмите хотя бы те 8 строк с которых началась статья и превратите их в шаблоны метапрограммирования в стиле C++98 — а дальше подумайте — легко ли там будет допустить ошибку и сколько времени вы будете её искать, если ошибётесь.

Вот самый очевидный пример который я придумал. В С++17 введена конструкция constexpr if. Она позволяет выполнять ветвления на этапе компиляции. Например:


template<class T>
auto call_java(T element){
     cout << "Start Java Call";
     constexpr if (is_same<T, int>){
         return jni->CallIntMethod(....);
     } else constexpr if (is_same<T, float>) {
         return jni->CallFloatMethod(....);
     } else {
         return jni->CallVoidMethod(....);    
     }
}

Без constexpr if вам бы пришлось в данном случае три дополнительные функции:


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(); }
     );
}

С увеличением сложности разница ещё более увеличивается. Основной плюс как по мне что код получается не размазанным по всему файлу.

Вот. Другое дело. Теперь видно, что вещь нужная. Спасибо за конструктивный ответ!
Кстати, на мой взгляд, в статье стоило именно этот пример привести. Он менее абстрактный и даёт чётко понять красоту придуманного вами механизма на прикладной задаче.
Ещё бы иметь возможность сделать как-нибудь в духе

...
[](Vec<float, n> element) { return jni->CallFloatVector(element, n); }
...


Но частичная специализация вроде до сих пор только для структур/классов возможна.
Но частичная специализация вроде до сих пор только для структур/классов возможна.
Для обычных функций ещё возможна. Для функций членов и лямбд — невозможна. Не знаю — в чём там засада.
Не нашёл, как возможна для обычных функций? Везде пишут, что нельзя, можно только полностью специализовать.
Сходил почитал доки. Вы правы, конечно. То, что можно делать (и что мы у себя в проекте, в частности) — называется function template overloading — это более общая вещь, в общем-то: вы можете указывать не только «уточнения», как с классами, но и гораздо более общие вещи… и становится немного яснее почему для членов класса она не очень хороша — хотя полного понимания таки нет: почему бы не разрешить это делать хотя бы с описанием в самом классе всех вариантов?

Всё-таки без полноценных типов-сумм сопоставлению с образцом очень не хватает юзабельности. То есть в таком виде это скорее static visitor от одного параметра.

Возможно можно как-то переделать на матчинг по нескольким параметрам (вас наверное деструктуризация интересует). Но! Рекомендую к ознакомлению http://cppnow2016.sched.org/event/6Sfb (когда появится конечно).

Тут выше был вариант с наследованием от лямбд, там в принципе можно сколько угодно аргументов протаскивать, лишь бы нашлась перегрузка нужная.

Мда действительно, просто то таки посмотрел на эту перегрузку другими глазами.

Я года 2 назад решал схожую задачу. Решил сделать статический маппинг функций на типы. Плюсом решения было свободный вызов любых Callable объектов, даже с одинаковой сигнатурой. Ну и редактор кода по выбранному ключу уже подсказывал типы и количество параметров. Выглядело примерно так:
//какие-то функции
int test1(float a, double b)
{
	std::cout << "int test1(float a, double b)\n";
	return 1;
}

void test2(std::string a, int b, const char* c)
{
	std::cout << "void test2(std::string a, int b, const char* c)\n";
}
...
//создаем "статическую мапу функций"
tagged_functor<
		int, decltype(test1),
		float, decltype(test2)> 
		functor(test1, test2);
....
//используем
functor.call<int>()(0.0f, 1.0);

To DISaccount:
А как вы решали дело с классами? Через указатель на член класса &ClassName::test1?

1) Простой bind.
2) Передача указателя на класс в вызове, как дополнительный параметр.
UFO just landed and posted this here
Sign up to leave a comment.

Articles