Посравниваем несравнимое? ) или немного метапрограммирования

Первая заметка за жизнь в этом блоге, но в ней будет и полезное про программирование)

Недавно довелось быть приглашенным в очень интересный проект, спасибо одному форму. Не знал что такое работает - просто написал мол "изучаю ++, буду рад принять участие в любом проекте на этом языке". Писал даже без надежды, просто так.. В итоге совершенно не ожидал что после этого:

  1. Действительно можно быть приглашенным в проект.
  2. Проект окажется не херней типа собрались джуны писать примитивную игрушку*/мессенджер/шифратор и т.п... (т.е. не бессмысленным, а вполне себе и технологически качающим скилл, и более того имеющим шанс по завершению на монетизацию, или по крайней мере на востребованность в рамках free software).
  3. То что буду приглашен не в один проект, при том новые приглашения будут не в менее крутые проекты, даже такие, которые уже приносят монеты их авторам**.

* - впрочем буду крайне рад присоединиться к любому геймдев Си++ проекту, любой сложности, а в скоре, как будет свободнее со временем буду уделять время разработке своих собственных игрушек, идеи есть еще с прошлой зимы, но пока не довелось...
**, хех, помню что кто-то говорил фриланс на Си и Си++ - миф. Не миф, но найти такой фриланс редок, очень.

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

Думаю уже не один программист, который становиться чуть опытнее студента - начинает осознавать что, как ни странно, для серьезного продвижения в программировании - знание английского намного важнее знаний математики.

За жизнь закончил. К сути.

В проекте существует свой строковый класс. Некоторый аналог std::string. И это не велосипедация (хотя не исключаю что наверняка есть сторонние либы для сего, но смысла узнавать о них нет, и значительно лучше написать свой класс (т.к. это проще чем загуглить в виду простоты класса), к тому же он адаптирован под местный (завроде как крутой) аллокатор, который даёт хороший прирост производительности. /*Эх скорее бы поизучать аллокаторы, но все не до них.*/.) Стандартный же стринг не угодил тем что в виду неконстантности длинны символа в кодировке utf8 std::string методы поиска и модификаций строки, в сигнатуре которых в качестве указателей на элементы - индексы (да и итераторы, т.к. стринговские итераторы - это однобайтовые прыжки) не будут работать корректно.

Ну дело за дело, наш стринг готов, и совместим с STL алгоритмами (если кто таким не занимался - то сами всё для себя откроекте, какие методы и операции должны быть в классе, какие подклассы и от чего должны наследоваться - это приятная такая задача, по совмещению, мне правда такое нравится - люблю копаться в STL).

Естественно над контейнерным классом должны быть определены операции сравнения. Крайне удобно определять их вне класса именно такой политики следуют в STL для стринга, правда там совсем всё просто - существует две перегруженных версии каждой операции - для перетасовки типов элементов - ну си_строка, стринг или стринг, си_строка - в общем понятно для чего - для независимости от порядка типов разных классов.

Наш же класс должен уметь сравниваться не только со стандартными си строками, но и c std::string. Правда я решил пойти дальше и написать операторы сравнения всех поледовательных контейнеров, причем только таких, которые содержат в себе char.

Понятно что перегрузка здесь не подойдет. Вернее подойдет - хотя ж задолбаешься копипастить, а если где то баг - исправлять все копипасты...

Выход? Шаблоны!

Сделаем это:

  1. template <class T1, class T2> bool operator == (T1 & s1, T2 & s2)
  2. {
  3. ...
  4. };

Казалось бы проблема решена, но нет. В большом проекте такой код может привести к подобным сюрпризам:

  1. error: cannot convert ‘doxyCraft::File**’ to ‘const char*

т.е. т.к. такой шаблон всеяден и присутствует в области видимости общей с некоторый другим классом, то определенные конкретно над тем классом операции сравнения перебиваются этим шаблоном - а он внутри себя не расчитан (ну просто не умеет) отобразить указатель на указатель файла на символьный массив. Отсюда следует то что данный шаблон перебьет вообще все другие операции сравнения, что очень не хорошо, даже если бы всё работало.

Вывод, нужно как-то ограничить использование такого шаблона. Чтобы он применялся именно тогда когда нужно. Я просто приведу этот код, т.к. при условии прочтения этой и этой темки из этого блога, понимание как работает данный код не должно вызвать вопросов.

  1. template < class T1, class T2 > struct check_types_helper
  2. {
  3. template<typename T> struct _is_array
  4. {
  5. private:
  6. template<typename C> static std::true_type test(C[]);
  7. template<typename C> static std::false_type test(...);
  8. public:
  9. typedef decltype(test<T>(0)) type;
  10. };
  11.  
  12. template<typename T> struct _is_container
  13. {
  14. private:
  15. template<typename C> static std::true_type test
  16. (typename std::iterator_traits <typename C::const_iterator>::pointer);
  17.  
  18. template<typename C> static std::false_type test
  19. (...);
  20. public:
  21. typedef decltype(test<T>(0)) type;
  22. };
  23.  
  24. template <class T> struct remove_array_type { typedef T type; };
  25. template <class T> struct remove_array_type<T[]> { typedef const T * type; };
  26.  
  27. template <typename C> static std::true_type test
  28. (typename remove_array_type<C>::type);
  29.  
  30. template <typename C> static std::true_type test
  31. (typename std::iterator_traits <typename C::const_iterator>::pointer);
  32.  
  33. template <typename C> static std::false_type test
  34. (...);
  35.  
  36. static const char * tp_;
  37. typedef typename std::__and_
  38. <
  39. std::__or_<typename _is_array<T1>::type, typename _is_container<T1>::type>,
  40. std::__or_<typename _is_array<T2>::type, typename _is_container<T2>::type>,
  41. decltype(test<T1>(tp_)), decltype(test<T2>(tp_))
  42. >::type type;
  43. };
  44.  
  45. template < typename _InIter1, typename _InIter2 > using RequiredCharTypes =
  46. typename std::enable_if <
  47. check_types_helper<_InIter1,_InIter2>::type::value
  48. >::type;
  49. template<typename T1, typename T2> const char * check_types_helper<T1,T2>::tp_ = "char";

последние штрихи:

  1. // определяем два основных шаблон-оператора сравнения
  2. template <class T1, class T2, typename = doxyCraft::RequiredCharTypes<T1,T2> > bool operator < (T1 & s1, T2 & s2)
  3. {
  4. auto beg1 = doxyCraft::begin(s1);
  5. auto end1 = doxyCraft::end(s1);
  6. auto beg2 = doxyCraft::begin(s2);
  7. auto end2 = doxyCraft::end(s2);
  8. auto size1 = std::distance(beg1, end1);
  9. auto size2 = std::distance(beg2, end2);
  10. auto min = std::min(size1, size2);
  11. auto compare = memcmp(beg1, beg2, min);
  12. if ( !compare ) compare = size1 - size2;
  13. return compare < 0;
  14. }
  15.  
  16. template <class T1, class T2, typename = doxyCraft::RequiredCharTypes<T1,T2> > bool operator == (T1 & s1, T2 & s2)
  17. {
  18. return std::equal ( _length(s1)==_length(s1) && doxyCraft::begin(s1), doxyCraft::end(s1), doxyCraft::begin(s2) );
  19. }
  20. // остальные же определяем через два других
  21. template <class T1, class T2, typename = doxyCraft::RequiredCharTypes<T1,T2> > bool operator > (T1 & s1, T2 & s2)
  22. {
  23. return operator<(s2, s1);
  24. };

Всё. После таких манипуляций можно будет сравнивать любые последовательные контейнеры, но только те, которые внутри себя содержат char тип между собой. Все же другие типы сравнения будут игнорироваться. Внимательный читатель подумает про ассоциативные контейнеры. Да они тоже попадают в наши шаблоны, но по некоторым причинам такие ф-ии как doxyCraft::begin() не поддерживают их (почему - это не особо интересно), в итоге для ассоциативных мы получим что и если бы нашего метакласса не было :) Мы не пишем либу, в нашем коде мы вряд ли догадаемся строку сравнивать с std::set например. Если же области видимости для сравнений сета каким-то образом все же пересекутся (а это возможно только тогда (по непонятным причинам мы вдруг) будем сравнивать std::set с чем-то отличным от него самого) - ну тогда сделать запрет на ассоциативные, займет не более 6 строк, спасибо std::is_same или же std::is_convertible (по вкусу) и вариативным шаблонам логических условий.

Что приятно IDE успешно понимает авторский замысел и заботливо говорит что сравнивать можно только символьные контейнеры:

Еще немножко про ограничение типов в метапрограммировании:
Допустим мы хотим написать метафункцию возвращающую истину если объект ф-я, но не простую, а такую, которая сообщила бы нам что например сейчас мы попытались сравнить int с функцией. Мало ли, возможно это спасет тонны времени поиска почему где-то что-то не работает.

Итак:

  1. // bar - ф-я любой сигнатуры.
  2. constexpr bool val = foo(bar); // ok
  3. constexpr bool val2 = foo(1); // compile error

Это можно сделать красиво, со своим сообщением:

  1. template <class T> constexpr bool foo (T f)
  2. {
  3. static_assert
  4. (std::is_function<typename std::remove_pointer<T>::type>::value,
  5. "must be function");
  6. return std::is_function<typename std::remove_pointer<T>::type>::value;
  7. }

Это можно записать короче и сохранить несколько тактов времени компиляции, но слегка пожертвовав читаемостью ошибки:

  1. template<typename _Res, typename... _ArgTypes>
  2. constexpr bool foo(_Res ptr(_ArgTypes...))
  3. {
  4. return true;
  5. }

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

Намомню что в шаблонах параметр представляющий фунцкию может иметь аж целых три разных типа: 1. тип возвращаемого значения функции, 2. тип самой фунции (такого типа не существует за границами шаблона как типа языка Си++, над ним невозможны операции приведения типа даже в рамках одного и того же типа, т.к. это тип синглтон, подробно этот тип описан в упомянутых выше статьях), ну и 3й тип, всем привычный тип указателя на фунцию (своего рода аналог 2-го типа, но с указателем).

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

  1. template<typename _Res, typename... _ArgTypes>
  2. constexpr bool correct_foo(_Res ptr(_ArgTypes...))
  3. {
  4. return std::__and_<std::is_integral<_Res>,
  5. std::__bool_constant<(sizeof(_Res)>sizeof(int32_t))>
  6. >::value;
  7. }

В общем метапрограммирование рулит.