Метапрограммирование. Часть 2. Продвинутый уровень.

Базовые типы

В мире метапрограммирования 11-го стандарта "под капотом" все несколько сложнее, взамен простому объявлению __is_integer добавлен шаблон std::is_integral, А взамен объявления пустых структур, как типов истинности, появляется базовый класс std::integral_constant, содержащий несколько больше данных внутри себя, а соответственно и возможностей применения. Например - оператор преобразования к типу. И уже на базе него строятся типы истинности, хотя в общем идея абсолютно та же.

  1. template<typename _Tp, _Tp __v>
  2. struct integral_constant
  3. {
  4. // Тут будет хранится переданный тип но в виде статического члена (статика
  5. // нужна чтобы иметь возможность что-то хранить, не создавая экземплра объета)
  6. static constexpr _Tp value = __v;
  7.  
  8. // тут будет хранится описание переданного в шаблон типа
  9. typedef _Tp value_type;
  10. // Очень интересный подход - сохранение имени своего типа внутри себя
  11. // чем-то похоже на поле next в односвязном списке, только хранится не
  12. // объект а имя типа.
  13. typedef integral_constant<_Tp, __v> type;
  14. // Операция приведения к типу (может быть использована во время выполнения
  15. // и во время компиляции, но в инстанцировании шаблонных параметров, а в
  16. // constexpr функцих.
  17. constexpr operator value_type() const { return value; } // #001
  18. };
  19. // Инициализация статического элемента класса.
  20. template<typename _Tp, _Tp __v>
  21. constexpr _Tp integral_constant<_Tp, __v>::value;

#001 -- обратите на внимание на шаблонную обертку и сравните с этим

Определение типов (через актуализацию), которые будут являться истиной и ложью при процессе генерации кода:

  1. /// The type used as a compile-time boolean with true value.
  2. typedef integral_constant<bool, true> true_type;
  3.  
  4. /// The type used as a compile-time boolean with false value.
  5. typedef integral_constant<bool, false> false_type;
  6.  
  7. template<bool __v>
  8. using __bool_constant = integral_constant<bool, __v>;

Кстати, здесь мы видим using. В таком исполнении это ключевое слово идентично typedef, и является псевдонимом (alias) семейства шаблонов, в данном случае используется для сокрытия первого шаблонного параметра integral_constant, делая как бы частичное специализацию integral_constant под новым именем. Подробнее:
здесь и тут.

И вот так объявлен, стандартизованный __is_integer (is_integral):

  1. template<typename>
  2. struct __is_integral_helper
  3. : public false_type { };
  4.  
  5. template<>
  6. struct __is_integral_helper<bool>
  7. : public true_type { };
  8.  
  9. template<>
  10. struct __is_integral_helper<char>
  11. : public true_type { };
  12.  
  13. //.....
  14. //.....
  15. //.....
  16.  
  17. template<>
  18. struct __is_integral_helper<unsigned long long>
  19. : public true_type { };
  20.  
  21. template<typename _Tp>
  22. struct is_integral
  23. : public __is_integral_helper<typename remove_cv<_Tp>::type>::type
  24. { };

Сравните еще раз как это было в 98-м и в более новых стандартах - очень похоже. remove_cv - снимает константность и volatile спецификацию с типа, это нужно чтобы "очистить" имя типа, чтобы __is_integral_helper с ним правильно отработал.

constexpr

В Си++11 появилось новое ключевое слово constexpr - про его особенности есть длинная статья на Хабре, которую пока, увы не прочел.

Вкратце - этот спецификатор объявляет что выражение является константным, почти тоже самое что const, но классический const служит для указания константности объекта во время выполнения программы, а constexpr для описания константности во время компиляции. Разница между const и constexpr хорошо описана здесь.

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

Логические операции

В 11-м Си++ содержится весь необходимый набор логических операций (вероятно они не стандартизованы (т.к. их описания в интернете я не находил), но их замечательно можно использовать). Кстати 14-й стандарт, называют небольшим патчем к 11 (т.е. разница между 98 и 11 большая, а вот между 11 и 14 нет) =)

  1. template<typename... _Bn> struct __or_;
  2. template<typename... _Bn> struct __and_;
  3. template<typename _Pp> struct __not_;
  4. // данные структуры представляют собой тип true_type или false_type,
  5. // преобразовываться к bool типу, и могут быть инстанцированны
  6. // произвольным количеством параметров (кроме __not_), эмулируя стандартные логические операторы

Подробнее здесь.

Манипуляции с типами

  1. //Удаление модификатора const
  2. template<typename _Tp> struct remove_const
  3.  
  4. //Снятие константности и volatile спецификации с типа
  5. template<typename _Tp> struct remove_cv
  6.  
  7. //Проверка на void тип
  8. template<typename _Tp> struct is_void
  9.  
  10. //Определение на массив
  11. template<typename _Tp> struct is_array
  12.  
  13. //Определение типа ссылки (rvalue/lvalue)
  14. template<typename _Tp> struct is_lvalue_reference
  15. template<typename _Tp> struct is_rvalue_reference
  16.  
  17. //Определение является ли элемент ссылкой
  18. template<typename _Tp> struct is_reference
  19.  
  20. //Определение является ли объект функцией/функциональным объектом или методом
  21. template<typename _Res, typename... _ArgTypes> struct is_function
  22.  
  23. //Определение является ли объект ли этот тип
  24. template<typename _Tp> struct is_object
  25.  
  26. //Определение может ли объект передаваться по-ссылке
  27. template<typename _Tp> struct __is_referenceable
  28.  
  29. //Добавление lvalue и rvalue ссылки к объекту
  30. // add_lvalue_reference
  31. template<typename _Tp> struct add_lvalue_reference
  32. // add_rvalue_reference
  33. template<typename _Tp> struct add_rvalue_reference

Подробнее здесь.

Проверка возможности преобразования одного типа в другой

  1. template<typename _From, typename _To>
  2. struct is_convertible
  3. : public __is_convertible_helper<_From, _To>::type
  4. { };
  5. // Эта структура наследует свойства типа true_type или false_type

Определение базового класса для is_convertible

  1. template<typename _From, typename _To,
  2. bool = __or_<is_void<_From>, is_function<_To>,
  3. is_array<_To>>::value>
  4. struct __is_convertible_helper
  5. { typedef typename is_void<_To>::type type; };
  6.  
  7. template<typename _From, typename _To>
  8. class __is_convertible_helper<_From, _To, false>
  9. {
  10. template<typename _To1>
  11. static void __test_aux(_To1);
  12.  
  13. template<typename _From1, typename _To1,
  14. typename = decltype(__test_aux<_To1>(std::declval<_From1>()))>
  15. static true_type
  16. __test(int);
  17.  
  18. template<typename, typename>
  19. static false_type
  20. __test(...);
  21.  
  22. public:
  23. typedef decltype(__test<_From, _To>(0)) type;
  24. };

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

  1. template<typename _From, typename _To,
  2. bool = __or_<is_void<_From>, is_function<_To>,
  3. is_array<_To>>::value>
  4. // если первый void или второй ф-я или массив, то тут (в bool типе шаблона) будет истина
  5. // ( как мы видели выше эти классы преобразуемы к стандартному типу bool,
  6. // но тут ручное обращение к значению )
  7. //
  8. // Основная специализация будет инкапсулировать истину
  9. // если второй парамерт тоже void (учтём что просто void переменных не существует)
  10. //
  11. // Cтоит понимать что is_void<T>, где T есть тип ф-ии (очищенной от указателя, не очищенная,
  12. // понятно что не подойдет тоже, т.к. буде являться указателем). Очищение, напомню делается
  13. // так decltype(/*имя void функции*/) – не подойдет для отработки is_void, т.к. в T будет тип ф-ии
  14. // (как говорилось выше это отдельный тип, существующий в шаблонах), проверять же тип
  15. // ф-ии (в том смысле тот, который она возвращает) можно таким способом
  16. // std::is_convertible<void,decltype(foo())>::value, или foo()(), если foo функтор. При этом стоит
  17. // помнить, что функторы не являюся ф-ями.
  18. // В общем говоря в этой основной версии шаблона на истину работает только
  19. // void->void ну и при некоторых внешних условиях void -> foo() | -> foo()()
  20. // – где foo() «вызвана» через decltype и является void ф-ей | функтором. Всё.
  21. //
  22. // Массивы же не могут быть void.
  23. // Могут быть void* но данный случай - _From=void*, _To=void* array - обработается не
  24. // в первой версии а в перегрузке шаблона, т.к. is_void не умеет работать с указателями
  25. // на void* но успешно обработается ниже.
  26. // Пруф для проверки: template <class A, class B>void foo2(A a, B b){ a = b; }
  27. // здесь оба элемента уже указатели, поэтому "возможно невозможное"
  28. // arr /*int arr[]*/ = ptr /*int *ptr*/ так же и с void за исключением того что void arr[]
  29. // невозможен, но возможен void * arr = new some_one[count] - что по сути будет
  30. // void arr[]. Т.е. (_From=void*, _To=void* array) == (_From=void*, _To=void array[])
  31. //
  32. // Understand? ) идем дальше, самое интересное впереди!
  33. //
  34. // во всех других случаях (преобразование к ф-ии или к массиву), внезависимости от
  35. // типа _From итоговый результат будет false_type, т.к. не существует преобразования
  36. // в указанные типы (не возможно что-то преобразовать в ф-ю* или массив**)
  37. // ** кроме случая показанного выше int[]=int* или void*/*==void[]*/=void*.
  38. // * is_convetible<decltype(foo),decltype(foo)> - будет false - т.к. тип ф-й не существует
  39. // вне шаблонов, и компилятор не умеет проверять возможность такого
  40. // неявного преобразования
  41. struct __is_convertible_helper
  42. { typedef typename is_void<_To>::type type; };
  43.  
  44. // просто на память - void невозможно преобразовать ни во что, т.к. это пустой тип
  45. // и ничто нельзя преобразовать в void (не путать с void*), к которому можно неявно
  46. // преобразовать что угодно, но в чтолибо из void* преобразовать неявно нельзя.
  47.  
  48. // Посмотрим на специализацию:
  49. template<typename _From, typename _To>
  50. class __is_convertible_helper<_From, _To, false>
  51. {
  52. // объявляем ф-ю, но она останется только объявленной, её определение не нужно
  53. // объявление будет служить для осуществления возможности передавать в неё
  54. // параметры...
  55. template<typename _To1>
  56. static void __test_aux(_To1);
  57.  
  58. // т.к. она статическая, мы можем обращаться к ней не создавая реальные объекты...
  59.  
  60. // через обращение к __test_aux объявляем шаблон для __test
  61.  
  62. // далее, т.к. __test_aux должен быть актуализироан _To1 а в _To1 передается _From1
  63. // то такой путь компилятор выберет лишь в том случае когда такое преобразование
  64. // возможно. В случае если же оно не возможно, компилятор выберет версию ф-ии с
  65. // интерфейсом со множественной передачей параметров
  66. // ( изначально она не выбирается, т.к. предпочтитительнее именно с одиним
  67. // параметром )
  68.  
  69. // зачем нужен 3-й параметр при инстанцировании? да потому что ф-ии не имеют тела,
  70. // и связать вызов одной ф-ии из другой (не имеющих тела) можно через
  71. // такую вот шаблонную связь. Хитро, хитро :)
  72.  
  73.  
  74. // также стоит понимать и помниь что в _From1 лежит описание типа, а не конкретная
  75. // переменная, например при вызове конструктора дека, в первый параметр попадет
  76. // vct1.begin().
  77. // _From1 же будет не результатом вызова vct1.begin(), а его типом, т.е.
  78. // std::vector<int>::iterator.
  79. template<typename _From1, typename _To1,
  80. // #002 #004 #003
  81. typename = decltype( __test_aux<_To1>( std::declval<_From1>() ) )> // #004
  82. static true_type
  83. __test(int);
  84.  
  85. template<typename, typename>
  86. static false_type
  87. __test(...);
  88.  
  89. public:
  90. typedef decltype(__test<_From, _To>(0)) type;
  91. };

#002 - оператор (с++11) decltype объявляет тип по выражению. Чем-то похоже на typedef, но typedef - определяет синоним типа по имени типа, а decltype объявляет тип переданного в него объекта. Пример.

decltype в данном случае необходим чтобы в typename записать тип (void), вызвать без decltype сразу на прямую __test_aux - невозможно, т.к. в типовой параметр шаблона может передаваться только имя типа, а не объект типа (если бы стояло например не typename=, а _To1= (т.е. некоторое заранее известное имя типа, без слов class или typename - то это был бы уже не типовой, а назовём его объектный параметр шаблона (далее в статье он будет зваться "нетиповым"), в который уже можно передавать конкретные объекты.

#003 - почему бы не вызвать сразу __test_aux<_To1>(_From1()) и тем самым предоставить доступ к типу, который возвращает __test_aux. Такой вызов возможен, и в большинстве случаев отработает, но это не универсальное решение, т.к. _From1 возможно будет являться классом, не имеющим конструктора по-умолчанию.

/* Кто-то может подумать "а зачем вообще конструировать объект, ведь можно передать сразу имя типа - т.е. _From1" - нельзя - т.к. это лишь описание типа, а для анализа нам нужно указать компилятору что-то более вещественное и ощутимое чем просто описание типа - а это есть создание элемента данного типа. Это можно воспринять как инициализацию константного выражения (времени компиляции), по которому будет строиться дальнейший алгоритм анализа. */

Именно для этой цели используется std::declval.

Как это работает:

declval это создающая функция, которая говорит компилятору, что он, после её вызова, имеет дело с элементом типа std::add_rvalue_reference::type - т.е. правосторонней ссылкой на объект. Если тип представлен как правосторонне-ссылочный, то компилятор воспринимает его как некоторый уже существующий объект-константу. Так и происходит "обман", и возможность получать экземпляр класса, который фактически не может быть создан через _From1() - т.е. конструирование по-умолчанию. Если посмотреть на объявление declval- то это, грубо говоря, пустая ф-я, которая синтаксически "обманывает" компилятор в том вопросе, что ничего не создает, но в сигнатуре возвращаемого значения имеет правостороннюю ссылку на объект (т.е. компилятор такую ссылку воспринимает как то что объект уже создан), поэтому её применяют для определения типа класса (объекта класса), или для доступа к методам или полям класса, не имеющего конструктора по умолчанию, с целью получить из них тип, который они возвращают), при этом это только интроспективный доступ к типам полей и методов, использовать метод или поле объекта такого класса невозможно (на самом деле возможно, но от такого использования стоит защита). Подробнее про работу std::declval можно прочитать здесь.

/* Для понимания всей механики, нужно помнить, что это касается обычных методов в классе, что к ним невозможно получить доступ (и к их типам) не создав объект - это хорошо видно в примере тут, а вот к статическим методам и их типам, естественно, можно получить доступ, не создавая объект вообще, или не эмулируя его создание, как это делает std::declval */

таким образом std::declval<_From1>() гарантирует что объект, даже если он не имеет конструктора по-умолчанию будет "создан" - а точнее не создан, а описан, причем как созданный, и компилятор начнет пробовать передать этот элемент в __test_aux, с оценкой того возможно ли его передать (полное соответствие типов или возможность неявного преобразования) или же, в случае невозможности - будет выбрана вторая версия ф-ии __test.

#004 - в коде видно немного странное обращение к вызову шаблона функции __test_aux<_to1>(some value)

  1. __test_aux<_To1>(some value)

Обычно во большинстве статей и учебников можно видеть только такое использование шаблонный ф-й

  1. __test_aux(some value)

Но существует способ принудительного вызова нужной перегрузки (т..е перегрузка генерируетcя автоматически)

Например есть шаблон

  1. template <class T> void foo (T v) { std::cout << "V1 " << v << std::endl; }

Если его использовать так

  1. foo(13.5);

То выведется V1 13.5 как и должно быть. А если так:

  1. foo<int>(13.5);

То автоматически создастся версия инстанцированная для типа int (и имеющая тело базовой версии), double значение неявно будет преобразовано к int, и на экран выведется V1 13.

Если интересно - можно поиграться с этим:

  1. template <class T> void foo (T v) { std::cout << "V1 " << v << std::endl; }
  2. template <> void foo<int> (int v) { std::cout << "V2 " << v << std::endl; }
  3. ...
  4. foo<int>(13.5); // V2 13
  5. foo(static_cast<int>(13.5)); // V2 13
  6. foo(13.5); // V1 13.5

Таким образом, здесь

  1. __test_aux<_To1>(some value)

Создается специализация для конкретного типа _To1, для проверки возможности передачи в него some value.

Ещё возможно и такое :)

  1. template <class T> void bar () { std::cout << "B1\n"; }
  2. template <> void bar<int> () { std::cout << "B2\n"; }
  3. ...
  4. bar<int>(); // B2
  5. bar<double>(); // B1

Но я отвлекся, продолжаем по-теме...

SFINAE

SFINAE - это свойство, доступное в Си++ довольно таки давно, в том числе и в стандарте 98.

Попросту говоря - это соглашение, при котором ошибки возникшие во время инстанцирования шаблона, связанные с отстутсвием неободимых данных (внутри других данных - т.е. например класс A не содержит поле B, хотя при инстанцировании идёт обращение к полю B) - не считаются ошибками, а шаблон, который невозможно инстанцировать по причине отсутствия необходимых данных просто исключается из списка кандидатов на перегрузку. SFINAE применяется только для перегруженных версий функций/методов, причем только шаблонов. /*т.е. если в ф-ии или методе foo применяется sfinae то эта ф-я/метод должна являться шаблоном, и при этом должна являться перегруженной версией такой же foo либо тоже являющейся шаблоном, либо не шаблонной версией. Иначе компилятор не сможет выбрать правильный путь и выдаст ошибку */ SFINAE использует автоматический вывод типа по входному аргументу функции компилятором. Поэтому SFINAE может применяться только при инстанцировании шаблонов функций или методов классов но не при специализации шаблонов самих классов. Но возможны трюки, когда можно выбирать на стадии компиляции по наличию или отсутствию полей основную или специализировнную версию шаблонного класса, для этого можно пользоваться создающими функциями, работающими с sfinae и создающие нужную версию шаблонного класса, или же определение наличие типа внести в параметр шаблона класса, как это сделано в iterator_traits

SFINAE работает только на "первом уровне" - т.е. может использоваться только среди параметров описания шаблона, или описания возвращаемого ф-ией значения, или среди параметров функции/метода, и проявляет себя только в процессе инстанцирования. Поэтому схожее обращение A::B в теле функции/класса, при условии что А не содержит B приведет к ошибке компиляции, т.к. тут уже никакого SFINAE не работает.

В Си++11 в стандарт был включен класс std::enable_if

  1. template <bool C, typename T = void> struct enable_if{};
  2. template <typename T> struct enable_if<true, T> { typedef T type; };

Который весьма удобно инкапсулирует в себе принципы SFINAE для определения стоит ли исключать данный объект из кандидатов на перегрузку.

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

  1. #include <iostream>
  2. #include <vector>
  3. #include <utility>
  4.  
  5. template <class T> class deque
  6. {
  7. public:
  8. deque ( unsigned a, int b) { std::cout << "1\n"; } //1
  9.  
  10. template
  11. <
  12. class _InIter, typename =
  13. typename std::enable_if
  14. <
  15. std::is_convertible
  16. <
  17. typename std::iterator_traits<_InIter>::iterator_category,
  18. std::input_iterator_tag
  19. >::value
  20. >::type
  21. >
  22. deque(_InIter _first, _InIter _last) { std::cout << "2\n"; } //2
  23.  
  24. };
  25.  
  26. int main()
  27. {
  28. deque<int> dq1 (10,-1); //1
  29.  
  30. int arr[]={1,2,3,4};
  31. deque<int> dq2 (arr,arr+4); //2
  32.  
  33. std::vector<int> vct1 (arr,arr+4);
  34. deque<int> dq3 (vct1.begin(), vct1.end()); //2
  35. }

Всё очень красиво и понятно. Сначала std::is_convertible осуществляет проверку на возможность преобразования итератора к типу input_iterator_tag.

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

Т.к. при передаче в параметр ф-ии объект неявно преобразуется к базовому, то при работе с переменной, в которой содержится копия переданного объекта - все методы (и обычные и виртуальные) и поля объекта будут именно базового типа, как и сама копия, будет без всякого следа того, что она "родилась" от производного класса.

Проверить возможность передачи просто:

  1. struct iinput_iterator_tag {};
  2. struct rrandom_access_iterator_tag : public iinput_iterator_tag {};
  3. void test (iinput_iterator_tag d) { std::cout << typeid(d).name() << '\n'; }
  4. ...
  5. test (rrandom_access_iterator_tag()); // -- Ok, преобразование типа к базовому классу

А далее std::enable_if определит, включится ли или будет исключена данная инстанция шаблона их кандидатов на перегрузку.

Кстати, предварительно объявив такой шаблонный псевдоним к std::enable_if

  1. template<typename _InIter>
  2. using _RequireInputIter = typename
  3. std::enable_if <
  4. std::is_convertible <
  5. typename std::iterator_traits<_InIter>::iterator_category, std::input_iterator_tag
  6. >::value
  7. >::type;

2-й конструктор можно записать значительно красивее:

  1. template<class _InIter, typename = _RequireInputIter<_InIter> >
  2. deque(_InIter _first, _InIter _last) { std::cout << "2\n"; }

И вот, благодаря SFINAE и шаблонам с параметрами по умолчанию для функций в Си++11 конструкторы дека будет исполнять именно свои роли, в отличии, от упомянутого из первой части статьи.

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

Так же, т.к. в шаблонах функций нет параметров по-умолчанию, то SFINAE не может работать на уровне описания параметров шаблонов, а только на уровне интерфейса ф-ий. При этом конечный параметр, играющий роль "определителя" включать или исключать из сборки - удобно объявить указателем, чтобы к нему можно было применить значение по-умолчанию, равное нулю.

Различие подхода к SFINAE в 98 и последующих стандартах хорошо описываются тут и тут (особенно видно на 19-й стр)

  1. #include <iostream>
  2. #include <iterator>
  3. #include <vector>
  4. // относительно длинный блок объявлений упомянутых выше структур,
  5. // которых нет в 98-м стандарте.
  6. //...
  7. // т.к. у нас нет is_convertible (а её нет, из-за отсутстия decltype)
  8. // то, чтобы не придумывать аналог, будем использовать перегрузку:
  9. template <class X> struct convertible_to_input_iter:
  10. public false_type {};
  11.  
  12. template <> struct convertible_to_input_iter
  13. <std::input_iterator_tag>: public true_type {};
  14.  
  15. template <> struct convertible_to_input_iter
  16. <std::random_access_iterator_tag>: public true_type {};
  17.  
  18. template <> struct convertible_to_input_iter
  19. <int*>:public true_type {};
  20.  
  21. template <class T> class deque
  22. {
  23. public:
  24. deque ( unsigned a, int b) { std::cout << "1\n"; }
  25. template <class _InIter> deque ( _InIter a, _InIter b,
  26. typename enable_if<
  27. convertible_to_input_iter< typename std::iterator_traits<_InIter>::iterator_category>::value
  28. >::type* = 0 )
  29. { std::cout << "2\n";};
  30. };
  31.  
  32. int main()
  33. {
  34.  
  35. deque<int> dq1 (10,-1); //Error:
  36. // error: ‘int’ is not a class, struct, or union type
  37.  
  38. int arr[]={1,2,3,4};
  39. deque<int> dq2 (arr,arr+4); //2
  40.  
  41. std::vector<int> vct1 (arr,arr+4); //
  42. deque<int> dq3 (vct1.begin(), vct1.end()); //2
  43. }

Видем ошибку, кстати, которой нет в С++11 - все потому что в 11-м iterator_traits дружелюбен к SFINAE :)

Решением данной проблемы может послужить обертка, с необходимым набором перегрузок для указания типа итератора.
И соответственно итоговый код второго конструктора, при котором в Си++98 будут вызваны именно те конструкторы, которые предполагает интерфейс.

  1. template <class _InIter> deque ( _InIter a, _InIter b,
  2. typename enable_if<
  3. convertible_to_input_iter<
  4. typename iterator_traits_proxy<_InIter>::iterator_category>::value
  5. >::type* = 0 )
  6. { std::cout << "2\n";}

В данном решении необходимо создать шаблон iterator_traits_proxy, который будет определять является ли его параметр актуализации обычным типом, указателем или итератором и в iterator_category размещать тип нужный для работы convertible_to_input_iter.

/* Кстати, изучая данную темку обнаружил что: в предыдущем стандарте в константных выражениях можно было использовать переменные только целого типа или типа перечисления а в Си++11 это ограничение снято (ссылка на вики). Имеется в виду та проблема, с которой столкнулся человек здесь - т.е. статические константы, которые _определяются_ прямо в классе, а не за его пределами. Но, во всех моих версиях компиляторов - даже для стандарта 98 года такие статические константы могут быть иметь любые типы, но кроме константных массивов, в 11-м же все можно, с constexpr. Подробнее тут.*/

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

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

Итоги

На примере STL мы подробно разобрали SFINAE, и служащаю для sfine технику применения шаблонов и специализаций для определения типов и рассмотрели как на основе шаблонов можно определить логические операции &&, ||, и !.

Стоит запомнить:

  • В метапрограммировани оперируют типами и константами времени компиляции.
  • Техника определения типа состоит в определении общего шаблона для всего, что не этот тип и в специализации шаблона, которая соответствует этому типу.
  • С помощью этой же техники реализованы преобразования типов, например std::remove_pointer и все другие, подобные задачи.

  • Шаблоны для своей актуализации могут вызывать другие шаблоны, а иногда и сами себя (см. __or_) в таком случае образуется рекурсивный вызов шаблоном самого себя. Условием выхода из рекурсии в метапрограммировании является специализация шаблона, которая останавливает рекурсию.
  • В вычислениях во время компиляции (о них пойдет речь ниже) так же часто применяется рекурсия.
  • В си++98 в качестве констант времени компиляции могли быть только целочисленные типы (существующие в классе как static), или как перечисляемый тип (который по определению константен). В последующих стандартах константами времени компиляции могут быть любые величины.
  • В 98-м невозможно было в вычислениях времени компиляции использовать вызов ф-й, которые что либо возвращают, в новых стандартах возможно, если ф-я объявлена как constexpr.
  • Типичный для 98-го стандарта способ определения итератор ли объект (использующий SFINAE):

    1. template<typename T>
    2. struct is_iterator
    3. {
    4. private:
    5. typedef char yes;
    6. typedef struct { char array[2]; } no;
    7.  
    8. template<typename C> static yes test(typename C::pointer);
    9. template<typename C> static no test(...);
    10. public:
    11. static const bool value = sizeof(test<T>(0)) == sizeof(yes);
    12. };
    13. template <typename T> bool iterator_type(T)
    14. {
    15. // т.к. компилятор умеет выводить из переменной её тип и автоматически
    16. // подставлять выведенный тип в типовые параметры шаблона только для
    17. // функций, то единственный способ передать тип существующего объекта
    18. // в инстанцирование шаблона - это обернуть создание объекта в
    19. // "создающую" ф-ю.
    20. return is_iterator<T>::value;
    21. }

    Кстати - виднен вызов ф-ии sizeof, а мы помним, что в 98-м нельзя использовать ф-ии в константных выражениях времени компиляции. И будь бы на месте sizeof какая-нибудь ф-я, возвращающая некий параметр - во время компиляции была бы ошибка (но, повторюсь, в новом стандарте её бы не было, если ф-я объявлена как constexpr). Дело в том что sizeof это не ф-я, а оператор языка, и при этом оператор времени компиляции:
    Т.е. данный код выведет только число 4 (размер типа ф-ии)

    1. int x(int a){
    2. std::cout << a;
    3. return a;
    4. }
    5. ...
    6. std::cout << sizeof ( x ( 20 ) );

    Типичный для новых стандартов способ:

    1. template<typename T>
    2. struct is_iterator
    3. {
    4. private:
    5. template<typename C> static true_type test(typename C::pointer);
    6. template<typename C> static false_type test(...);
    7. public:
    8. typedef decltype(test<T>(0)) type;
    9. };
    10. template <typename T> bool iterator_type(T)
    11. {
    12. return is_iterator<T>::type::value;
    13. }

От SFINAE к вычислениям во время компиляции

Всё что мы рассмативали выше - это рассматрение того, как "под капотом" работает sfinae. Естественно, метапрограммирование не ограничивается только преобразованиями типов. Уже на том что мы рассмотрели выше, можно увидеть ещё одну область метапрограммирования - а именно расчетов на этапе компиляции.

Мы видили это в этой строке. В ней вычисляется значение value.

  1. static const bool value = sizeof(test<T>(0)) == sizeof(yes);

После компиляции, в программе, все строки

  1. is_iterator<T>::value;

будут заменены вычисленным значением true или false.

Можно пойти дальше... Все что будет описано ниже, применимо и в 98 стандарте.

Классический пример - вычисление факториала, которое как правило значительно проще реализовать рекурсивно:

  1. int64_t factorial (int64_t n)
  2. { return (n==0) ? 1: n*factorial(n-1); }

Но это крайне нехорошее решение. Почему рекурсия времени выполнения это плохо - описывалось тут.

Вот решение получше:

  1. template <typename T> int64_t factorial(T N)
  2. {
  3. if (N < 0)
  4. return 0;
  5. if (N == 0)
  6. return 1;
  7.  
  8. int64_t result = 1ll; //long long == int64_t
  9. for (unsigned i = 1; i <= N; i++) {
  10. result *= i;
  11. }
  12. return result;
  13. }

Но, можно сделать решение, работающее ещё быстрее, чем итеративное вычисление факториала - а именно вычислять его на этапе компиляции:

  1. // шаблон для вычисления
  2. template <int n>
  3. struct factorial_c
  4. {
  5. enum { value = factorial_c<n-1>::value * n };
  6. };
  7.  
  8. // условие выхода из рекурсии
  9. template < >
  10. struct factorial_c<0>
  11. {
  12. enum { value = 1 };
  13. };

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

  1. template <int n>
  2. struct factorial_c
  3. {
  4. static const int64_t value = factorial_c<n-1>::value * n;
  5. };
  6.  
  7. template < >
  8. struct factorial_c<0>
  9. {
  10. static const int64_t value = 1 ;
  11. };

Теперь, на стадии выполнения программы вычислений не будет, т.к. в коде теперь везде вместо factorial_c<5>::value - будет результат вычислений.

Стоит обратить внимание на то что шаблонная переменная может иметь только явный тип т.к. только так можно инстанцировать класс константой (в таком случае передаваемое значение воспринимается не как имя типа, а как имя переменной, с которой можно производить, например, операции разности и произведения) /* причем только целочисленный, хоть в новом стандарте и заявлена поддержка переменных любого типа, но в параметрах шаблона, если объявляется жестко конкретный тип - он может быть только целочисленным */

Важные отличия между вычислениями на этапе выполения и вычислениями на этапе компиляции:

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

Вычисление выражений

Факториал это искусственный пример т.к. никто не будет вычислять факториал используя встроенные типы данных*, но пример этот крайне удачный чтобы ввести в суть. Так же на примере вычисления факториала - реализуется модель вычисления из одной константы - в другую.

/* Если далее если не очень понятен текст - то можно спустить ниже и посмотреть код, потом прочитать что было выше. =) */

Но вычисления на этапе компиляции можно применять и к объектам ещё не известным на этапе компиляции, в данном случае это можно воспринимать как аналог математического упрощения выражения. Например из рекурсивного выражения получить нерекурсивное. Т.е. вычисляться будет не конкретная константа, а будет генерироваться выражение, для её подсчета. В таком случае говорят о вычислении выражений на этапе компиляции.

И вот пример, который полезен и на практике - вычисление скалярного произведения N-мерных векторов. Более того этот пример, в отличии от факториала, который вычисляет значение на этапе компиляции, будет вычислять выражение на этапе компиляции.

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

Скалярное произведение — можно рассматривать как частный случай паттерна проектирования, называемого «Компоновщик» (composite). Данный паттерн представляет отношение «часть-целое» так, что клиент может игнорировать различия между отдельным объектами или композицией этих объектов. Ключевыми объектами являются leaf (лист, примитивный объект) и composite (композиция, составной объект).

  • Leaf — определяет поведение примитивных объектов.
  • Composite — определяет поведение компонентов, состоящих из листьев.

Примерами данного паттерна можно считать рекурсивные алгоритмы, рекурсивные структуры данных и синтаксические деревья.
Рис.1 Типичная структура составного объекта.
Рис.1 Типичная структура составного объекта.

В GoF описана объектно-ориентированная версия патерна Composite c абстрактным базовым классом, который определяет общую для leaf и composite операцию и два производных класса, определяющих leaf и composite.

Рис.2. Uml-диаграмма классов паттерна Composite.
Рис.2. Uml-диаграмма классов паттерна Composite.

Для скалярного произведения leaf — это скалярное произведение двух векторов размерности 1. А composite — скалярное произведение двух векторов размерности N-1.

Рис.3. Структура паттерна Composite для скалярного произведения.
Рис.3. Структура паттерна Composite для скалярного произведения.

Это вырожденная форма композита потому, что каждый составной объект состоит ровно из одного листа и одного композита. Поэтому диаграмма классов будет выглядеть так:

Рис.4. Uml-диаграмма классов скалярного произведения на базе паттерна Composite.
Рис.4. Uml-диаграмма классов скалярного произведения на базе паттерна Composite.

  1. // Базовый класс
  2. template <class T>
  3. class DotProduct
  4. {
  5. public:
  6. virtual ~DotProduct ( ) { }
  7. virtual T eval( ) = 0;
  8. };
  9.  
  10. // класс выдающий произведение двух точек (примитивный объект)
  11. template <class T>
  12. class SimpleDotProduct : public DotProduct <T>
  13. {
  14. public:
  15. SimpleDotProduct ( T * a, T * b ) : v1(a), v2(b) { }
  16. virtual T eval( ) { return (*v1)*(*v2); }
  17. private:
  18. T* v1; T* v2;
  19. };
  20.  
  21. // Класс связывающий произведения точек (составной объект)
  22. template <class T>
  23. class CompositeDotProduct: public DotProduct <T>
  24. {
  25. public:
  26. CompositeDotProduct ( T* a, T* b, size_t dim ):
  27. s(new SimpleDotProduct<T>(a,b)),
  28. c ( dim==1 ? 0 : new CompositeDotProduct <T> ( a+1,b+1,dim-1 ) )
  29. { }
  30. virtual ~CompositeDotProduct ( ) { delete c; delete s; }
  31. virtual T eval ( ) { return ( s->eval( ) + ( (c) ? c->eval( ) : 0 ) ); }
  32. protected:
  33. SimpleDotProduct <T> * s;
  34. CompositeDotProduct <T> * c;
  35. };
  36.  
  37. // вспомогательная функция
  38. template <class T> T dot ( T * a, T * b, size_t dim )
  39. {
  40. return (dim==1)
  41. ? SimpleDotProduct<T>(a,b).eval()
  42. : CompositeDotProduct<T>(a,b,dim).eval();
  43. }

Кстати, очень интересный подход - класс поражает сам себя рекурсивно в конструкторе!

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

  1. template <class T>
  2. class DotProduct
  3. {
  4. public:
  5. virtual ~DotProduct ( ) { }
  6. virtual T eval( T *, T *, size_t ) = 0;
  7. };
  8.  
  9. template <class T>
  10. class SimpleDotProduct: public DotProduct <T> {
  11. public:
  12. virtual T eval(T* a, T* b, size_t) { return (*a)*(*b); }
  13. };
  14.  
  15. template <class T>
  16. class CompositeDotProduct: public DotProduct <T>
  17. {
  18. public:
  19. inline virtual T eval ( T *, T *, size_t );
  20. };
  21.  
  22. template <class T> T CompositeDotProduct<T>::eval ( T * a, T * b, size_t dim )
  23. {
  24. return
  25. SimpleDotProduct<T>().eval(a,b,dim)
  26. + ( dim == 1 ? 0 : CompositeDotProduct<T>( ).eval ( a+1,b+1,dim-1 ) );
  27. }
  28.  
  29. // вспомогательная функция
  30. template <class T> T dot ( T* a, T* b, size_t dim )
  31. {
  32. return CompositeDotProduct<T>().eval(a,b,dim);
  33. }
  34.  
  35. // пример использования
  36. int a[4] = {1,100,0,-1};
  37. int b[4] = {2,2,2,2};
  38. std::cout << dot(a,b,4);

Рис.5. Uml-диаграмма классов упрощенной версии вычисления скалярного произведения.
Рис.5. Uml-диаграмма классов упрощенной версии вычисления скалярного произведения.

Два класса, представляющие leaf и composite имеют общий базовый класс, который определяет их операции. Это хорошо известная объектно-ориентированная техника: общности (общие методы для потомков) выражается с помощью общего базового класса.

В шаблонном программировании общности выражаются только через одинаковые имена и сигнатуры шаблонов.

Важные отличия между вычислениями на этапе выполения и вычислениями на этапе компиляции, касающиеся иерархии

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

В отличии от рекурсивной специализации для вычисления факториала ( передавалось лишь одно значение и результат вычисления складывался во вложенную константу класса ) здесь будет передаваться три значение, одно так же будет "нетиповым" параметром шаблона, указывающим его размер, а два других значения - суть вектора, которые будут передаваться в статический шаблонный вычислительный метод, т.к. это наиболее удобный способ передатать типонезависимо конкретные переменные в шаблонный объект:
DotProduct<N,T>::eval(a,b) где:
N - "нетиповой" параметр
T - типовой параметр, для абстрагирования указателей a и b от конкретного типа.

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

Рис.6. Uml-диаграмма классов реализации скалярного произведения, вычисляемого на этапе компиляции
Рис.6. Uml-диаграмма классов реализации скалярного произведения, вычисляемого на этапе компиляции.

Реализация:

  1. // базовый класс
  2. template <size_t N, class T>
  3. class DotProduct
  4. {
  5. public:
  6. inline static T eval ( T * , T * );
  7. };
  8.  
  9. template <size_t N, class T> T DotProduct<N,T>::eval ( T * a, T * b )
  10. {
  11. return DotProduct<1,T>::eval ( a, b )
  12. + DotProduct<N-1,T>::eval ( a+1, b+1 );
  13. }
  14.  
  15. // специализация
  16. template <class T>
  17. class DotProduct<1,T>
  18. {
  19. public:
  20. static T eval(T* a, T* b) { return (*a)*(*b); }
  21. };
  22.  
  23. // вспомогательная функция
  24. template <size_t N, class T> inline T dot(T* a, T* b)
  25. {
  26. return DotProduct<N,T>::eval(a,b);
  27. }
  28.  
  29. // использование
  30. int a[4] = {1,100,0,-1};
  31. int b[4] = {2,2,2,2};
  32. std::cout << dot<4>(a,b);

Посмотрим что происходит при вычислении на этапе выполнения:
dot(a,b,4); сводится к вычислению

  1. CompositeDotProduct<size_t>().eval(a,b,4)

что приводит к следующим рекурсивным вызовам на стадии исполнения:

  1. SimpleDotProduct <size_t>().eval(a,b,1)
  2. CompositeDotProduct <size_t>().eval(a+1,b+1,3)
  3. SimpleDotProduct <size_t>().eval(a+1,b+1,1)
  4. CompositeDotProduct <size_t>().eval(a+2,b+2,2)
  5. SimpleDotProduct <size_t>().eval(a+2,b+2,1)
  6. CompositeDotProduct <size_t>().eval(a+3,b+3,1)
  7. SimpleDotProduct <size_t>().eval(a+3,b+3,1)

что приводит в итоге к 7 вызовам виртуальных функций.

При вычислении выражения на этапе компиляции происходит:
Вызов dot<4>(a,b) сводится к вычислению

  1. DotProduct<4,size_t>::eval(a,b);

что запускает конкретизацию шаблонов, которая последовательно разворачивается следующим образом:

  1. DotProduct<4,size_t>::eval(a,b)
  2.  
  3. // приводит к
  4.  
  5. DotProduct<1,size_t>::eval(a,b) + DotProduct<3,size_t>::eval(a+1,b+1)
  6.  
  7. // что приводит к вычислению
  8.  
  9. (*a)*(*b) + DotProduct<1,size_t>::eval(a+1,b+1) +
  10. DotProduct<2,size_t>::eval(a+2,b+2)
  11.  
  12. // что приводит к вычислению
  13.  
  14. (*a)*(*b) + (*a+1)*(*b+1) + DotProduct<1,size_t>::eval(a+2,b+2) +
  15. DotProduct<1,size_t>::eval(a+3,b+3)
  16.  
  17. // что приводит к вычислению
  18. (*a)*(*b) + (*a+1)*(*b+1) + (*a+2)*(*b+2) + (*a+3)*(*b+3)

Видимым в исполняемом коде будет только результирующее выражение:

  1. (*a)*(*b) + (*a+1)*(*b+1) + (*a+2)*(*b+2) + (*a+3)*(*b+3)

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

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

Рекурсивная шаблонная конкретизация занимает больше времени и неудивительно, что требуемое время компиляции возрастает, для шаблонного решения по сравнению с объектно-ориентированным решением, которое компилируется быстрее, но работает медленнее.

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

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

  1. template <int N> int64_t foo_n ()
  2. {
  3. return foo_n<N-1>() * N;
  4. }
  5.  
  6. template <> int64_t foo_n<1> ()
  7. {
  8. return 1;
  9. }

В код подставится итеративный вызов конкретизаций этой функции, т.е. например foo<15>()...*foo<1>() - т.е. рекурсивная специализация метафункций основанных не на шаблонах классов а на основе шаблонов обычных ф-й (или нешаблонных ф-й вообще) может оказаться крайне неэффективным решением, но может и оказаться равноэффективным вычислениям в метаклассах. А почему так откроет эта короткая заметка, которая раскрывает подробности работы метафункций на основе обычных функций.

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

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

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

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

Арифметические выражения состоят из унарных и бинарных операций, переменных и констант. Арифметические выражения описывает паттерн Interpreter, описанный в GoF, являющийся частным случаям паттерна Composite.

Отношение “часть-целое” паттерна Composite соответствует отношению выражения и подвыражения в паттерне Intepreter.

  • Leaf будет терминальным выражением (т. е. прерывающим дальнейший синтаксический анализ) — это константы и переменные
  • Composite — это все то что не Leaf — т. е. унарные и бинарные операции.
  • Вычисление компонентов является интерпретацией синтаксического дерева и его выражений.

Синтаксическое дерево представляется такими арифметическими выражениями как (a+1)*c или log(abs(х-N)). Переменные могут меняться между вызовами сгенерированного выражения. Литералы (константы) — нет.

Будем рассматривать конкретный пример выражения — (x+2)*3

Составная структура данного выражения, или другими словами, синтаксическое дерево данного выражения представлена следующим образом:
Рис. 7. Пример синтаксического дерева для арифметического выражения.
Рис. 7. Пример синтаксического дерева для арифметического выражения.

Классическая объектно-ориентированная реализация Interpreter включает следующие классы:
Рис. 8. Uml-диаграмма классов объектно-ориентированной реализации интерпретатора арифметических выражений.
Рис. 8. Uml-диаграмма классов объектно-ориентированной реализации интерпретатора арифметических выражений

Реализация:

  1. #include <iostream>
  2.  
  3. struct AbstractExpr
  4. {
  5. virtual double eval() const = 0;
  6. virtual ~AbstractExpr(){}
  7. };
  8.  
  9. // данный класс (структура) автоматом наследует чисто виртуальный
  10. // метод eval поэтому опеределять/переопределять его не нужно.
  11. struct TerminalExpr : public AbstractExpr
  12. {
  13. };
  14.  
  15. struct NonTerminalExpr : public AbstractExpr
  16. {
  17. };
  18.  
  19. class Literal : public TerminalExpr
  20. {
  21. public:
  22. Literal (double v) : _val(v) {}
  23. double eval() const { return _val; }
  24. private:
  25. const double _val;
  26. };
  27.  
  28. class Variable : public TerminalExpr
  29. {
  30. public:
  31. Variable(double & v) : _val(v) {}
  32. double eval() const { return _val; }
  33. private:
  34. double& _val;
  35. };
  36.  
  37. class BinaryExpr : public NonTerminalExpr
  38. {
  39. protected:
  40. BinaryExpr ( const AbstractExpr * e1, const AbstractExpr * e2 )
  41. : _expr1(e1),_expr2(e2) {}
  42. virtual ~BinaryExpr ()
  43. { delete const_cast<AbstractExpr*>(_expr1);
  44. delete const_cast<AbstractExpr*>(_expr2);
  45. }
  46. const AbstractExpr* _expr1;
  47. const AbstractExpr* _expr2;
  48. };
  49.  
  50. class Sum : public BinaryExpr
  51. {
  52. public:
  53. // напомню что только через указатели или ссылки на базовый
  54. // возможна передача производного класса в тип базового.
  55. Sum ( const AbstractExpr* e1, const AbstractExpr* e2 )
  56. : BinaryExpr(e1,e2) {}
  57. double eval( ) const
  58. { return _expr1->eval() + _expr2->eval(); }
  59. };
  60.  
  61. class Product : public BinaryExpr
  62. {
  63. public:
  64. Product ( const AbstractExpr* e1, const AbstractExpr* e2 )
  65. : BinaryExpr(e1,e2) {}
  66. double eval( ) const
  67. { return _expr1->eval() * _expr2->eval(); }
  68. };
  69.  
  70. void someFunction(double x)
  71. {
  72. // демонстрация смысла передачи в Variable параметра по ссылке
  73. std::cout << "start value: " << x;
  74. Product expr(new Sum(new Variable(x),new Literal(2)), new Literal(3));
  75. std::cout << "\n#01: " << expr.eval();
  76. x += .002d;
  77. std::cout << "\nchanged value: " << x;
  78. std::cout << "\n#02: " << expr.eval();
  79. std::cout << std::endl;
  80. }
  81.  
  82. int main()
  83. {
  84. // (x+2)*3 where PI = π
  85. someFunction(3.14d);
  86. return 0;
  87. }

Сначала создается объект выражения expr, который представляет выражение (x+2)*3 и затем вычисляется объект выражения. При этом, x между обращениями к объекту выражения можно менять, т. к. он передан по ссылке.

Это не самое эффективное решение, но на основе него можно построить очень эффективное решение времени компиляции.

Как говорилось ранее — шаблонные решение основаны на общности имён и специализациях шаблонов — поэтому в базовых классах нет нужды.

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

Для описания классов литералов и переменных достаточно сходства имен и сигнатур их методов. Нетерминальные выражения такие как сумма, произведение и прочие будут определяться через конкретизации шаблонных классов BinaryExpr и UnaryExpr, которые конкретизируются структурной информацией — т. е. тем, что было нарисовано на рисунке 7. (будет понятно ниже). Эти шаблоны классов будут принимать типы их подвыражений как ”типовые” шаблонные аргументы. В последствии вычислительные методы классов нетерминальных выражений будут заменены на одноименные операции, что предоставит клиентам данного арифметического интерпретатора значительно более удобный интерфейс.

Рекурсия с этапа выполнения (вложенность (new Sum(new Variable(x),new Literal(2)), new Literal(3)) ) перейдет в рекурсию времени компиляции по инстанцированию шаблонов, правда рекурсия не будет похожа на ту, что мы рассматривали в этой статье, т. е. условие выхода не специализация шаблона — а дискретная структура синтаксического дерева — см рисунок 7 и код ниже.

Рис. 9. UML-диаграмма классов шаблонной реализации интерпретатора арифметических выражений.
Рис. 9. UML-диаграмма классов шаблонной реализации интерпретатора арифметических выражений.

Реализация:

Классы примитивных объектов (терминированных)

  1. class Literal
  2. {
  3. public:
  4. Literal ( const double v ) : _val(v) {}
  5. double eval() const { return _val; }
  6. private:
  7. const double _val;
  8. };
  9.  
  10. class Variable
  11. {
  12. public:
  13. Variable( double & v ) : _val(v) {}
  14. double eval() const { return _val; }
  15. private:
  16. double & _val;
  17. };

Класс бинарного выражения. По его подобию делается класс унарного выражения, но в изложении он отсутствует.

  1. template <class ExprT1, class ExprT2, class BinOp> class BinaryExpr
  2. {
  3. public:
  4. BinaryExpr ( ExprT1 e1, ExprT2 e2,BinOp op = BinOp() ) : // *#01
  5. _expr1(e1), _expr2(e2), _op(op) {}
  6. double eval() const
  7. { return _op ( _expr1.eval(),_expr2.eval() ); }
  8. private:
  9. ExprT1 _expr1;
  10. ExprT2 _expr2;
  11. BinOp _op;
  12. };


/*  #01
    3-м параметром, создаём вызов конструктора по умолчанию
    это избавит нас от необходимости в клиентах этого класса констрировать его так:
    BinaryExpr<ExprT1, ExprT2, std::plus<double> >(e1,e2, std::plus<double>()); */

Для операций в STL уже есть готовые функторы, мы будем использовать их, но в случае нехватки, конечно можно реализовывать и свои.

Чтобы упростить создание объектов выражений, и сделать возможным из переменных вывести имена их типов, будем использовать "создающую функцию" - довольно частый приём в том же STL.

  1. // helpers (make functions)
  2.  
  3. template <class ExprT1, class ExprT2> BinaryExpr < ExprT1, ExprT2, std::plus<double> >
  4. makeSum ( ExprT1 e1, ExprT2 e2 )
  5. {
  6. return BinaryExpr<ExprT1, ExprT2, std::plus<double> >(e1,e2);
  7. }
  8.  
  9. template <class ExprT1, class ExprT2> BinaryExpr < ExprT1, ExprT2, std::multiplies<double> >
  10. makeProd ( ExprT1 e1, ExprT2 e2 )
  11. {
  12. return BinaryExpr<ExprT1, ExprT2, std::multiplies<double> >(e1,e2);
  13. }

Использование шаблонного интерпретатора:

  1. void someFunction ( double x )
  2. {
  3. // объявление типа переменной expr
  4. BinaryExpr< BinaryExpr < Variable,Literal,std::plus<double> >,
  5. Literal,
  6. std::multiplies<double> >
  7. expr = makeProd ( makeSum ( Variable(x), Literal(2) ), Literal(3) );
  8. std::cout << expr.eval() << std::endl;
  9. }

В теле данной функции создастся объект, представляющий выражение (x+2)*3, а затем рассчитается само выражение. Помните текст "будет понятно ниже" - вот, теперь понятно о чем там говорилось. Мы конкретизируем шаблон структурной информацией, описывающей синтаксическое дерево - т.е. тип объекта выражения отражает структуру синтаксического дерева.

Вот тут как раз даже очень заядлые любители эстетики шаблонов будут рады введенной в 11-м стандарте auto переменной :)

  1. void someFunction ( double x )
  2. {
  3. auto
  4. expr = makeProd ( makeSum ( Variable(x), Literal(2) ), Literal(3) );
  5. std::cout << expr.eval() << std::endl;
  6. }

Что имеем?

Если компилятор встраивает все создающие функции, конструкторы и функции eval() (скорее всего так и будет, так как они тривиальныe (на моих тестах всраивает с ключом -O2) ) выражение

  1. makeProd ( makeSum( Variable(x), Literal(2) ), Literal(3) ).eval()

сводится к

  1. (x+2)*3

Это в разы эффективнее - чем решение, работающее во время выполнения программы:

  1. Product expr( new Sum ( new Variable(x), new Literal(2) ), new Literal (3) ).eval()

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

Решение на основе шаблонов выполняется гораздо быстрее, чем объектно-ориентированная реализация.

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

  1. makeProd ( makeSum( Variable(x), Literal(2) ), Literal(3) ).eval()

Для этого потребуется шаблонные создающие функции makeSum и makeProd сделать перегруженными шаблонными операциями, т. е. Просто поменять их объявление с makeSum и makeProd на operator + и operator * соответственно.
Теперь в использующей функции можно

  1. makeSum ( Variable(x), Literal(2)), Literal(3) ).eval()

заменить на

  1. ( Variable(x) + Literal(2) ) * Literal(3);

Удобнее. Идём далее. Хотелось бы, чтобы можно было писать нечто, максимально похожее на ((х+2)*3) — т. е. Цель устранить явное создание объектов переменных и объектов литералов, которые загромождают выражение.

Правда полностью отказаться от некоторого загромождения не получится, т. к. если использовать непосредственно ((х+2)*3), где xdouble, компилятор вызовет операцию + над типом double и выражение приведется к этому типу, и соответственно не сможет вызвать наш шаблоный вычислительный метод eval() времени компиляции. Переменную придется обернуть в Variable тип, чтобы выражение привелось к типу BianaryExpr.

Для конкретизации BinaryExpr в шаблон передаются конкретные типы Variable и Literal.

Если вместо

  1. ( Variable(x) + Literal(2) ) * Literal(3);

писать

  1. Variable v(x);
  2. std::cout << ((v+2)*3.0).eval() << std::endl;

То операция + (являющаяся создающей функцией) заставит компилятор вывести тип второго аргумента шаблона как тип int, так же, если не использовать обращение к безымянному объекту а создавать именованный объект, для возможности его повторного использования, то придётся инстанцировать шаблон не пользовательскими типами Literal для констант а именно типами этих констант:

  1. Variable v(x);
  2. BinaryExpr< BinaryExpr < Variable, int, std::plus<double> >,
  3. double,
  4. std::multiplies<double> >
  5. expr = (v + 2) * 3.0;
  6. std::cout << expr.eval() << std::endl;

Таким образом оба варианта использования, для нашего выражения, в шаблоне BinaryExpr _expr2 будет иметь тип int. Но int не имеет метода eval().

Тут необходима техника traits — описанная в этом блоге на примере итераторов (когда обычный указатель через обертку, наделялся аттрибутами, которые он не имел), и статье первоисточнике на примере шаблона string.

Но задача в отличие от iterator_traits чуть-чуть в другом - не снабдить указатель полями — описателями типов, а создать отображение констант в тип литералов, а для всех других типов (переменные и нетерминалы) сделать отображение в себя же:

  1. // Base Template of Traits:
  2. template <class Expr> struct exprTraits
  3. { typedef Expr type; };
  4.  
  5. // Specializations:
  6. template <> struct exprTraits <double>
  7. { typedef Literal type; };
  8.  
  9. template <> struct exprTraits <int>
  10. { typedef Literal type; };

Естественно нужно охватить все типы, мы же ограничимся только теми, которые используем в примере.

Поля в BinaryExpr с таких

  1. ExprT1 _expr1;
  2. ExprT2 _expr2;

Теперь станут такими

  1. typename exprTraits<ExprT1>::type _expr1;
  2. typename exprTraits<ExprT2>::type _expr2;

Теперь выражение, такое как (х+2)*3, будет вычисляться без лишних награмождений.

Если создать вспомогательную функцию

  1. template <class ExprT> double eval(ExprT e) { return e.eval(); }

то можно будет избавиться, от выглядящего странным — обращения к методу сущности, которая выглядит как выражение.

Теперь, вместо

  1. ((v+2)*3).eval()

можно писать

  1. eval( (v+2)*3 )

На рисунке показывается как выражение ((v+2)* 3).eval(), где v является Variable – оберткой double переменной x, постепенно разворачивается во время компиляции к выражению (x+2)*3.

Рис. 10. Вычисление объекта выражения (v+2)*3 на этапе компиляции.

Реализация:

  1. #include <iostream>
  2.  
  3. // Literal and Variable Class
  4. class Literal
  5. {
  6. public:
  7. Literal ( const double v ) : _val(v) {}
  8. double eval() const { return _val; }
  9. private:
  10. const double _val;
  11. };
  12.  
  13. class Variable
  14. {
  15. public:
  16. Variable( double & v ) : _val(v) {}
  17. double eval() const { return _val; }
  18. private:
  19. double & _val;
  20. };
  21.  
  22. // Base Template of Traits:
  23. template <class Expr> struct exprTraits
  24. { typedef Expr type; };
  25.  
  26. // Specializations:
  27. template <> struct exprTraits <double>
  28. { typedef Literal type; };
  29.  
  30. template <> struct exprTraits <int>
  31. { typedef Literal type; };
  32.  
  33. // Binary Expression Class
  34. template <class ExprT1, class ExprT2, class BinOp> class BinaryExpr
  35. {
  36. public:
  37. BinaryExpr ( ExprT1 e1, ExprT2 e2,BinOp op=BinOp() ) :
  38. _expr1(e1), _expr2(e2), _op(op) {}
  39. double eval() const
  40. { return _op ( _expr1.eval(),_expr2.eval() ); }
  41. private:
  42. typename exprTraits<ExprT1>::type _expr1;
  43. typename exprTraits<ExprT2>::type _expr2;
  44. BinOp _op;
  45. };
  46.  
  47. // Helpers (make functions)
  48. template <class ExprT1, class ExprT2> BinaryExpr < ExprT1, ExprT2, std::plus<double> >
  49. operator + ( ExprT1 e1, ExprT2 e2 )
  50. {
  51. return BinaryExpr<ExprT1, ExprT2, std::plus<double> >(e1,e2);
  52. }
  53.  
  54. template <class ExprT1, class ExprT2> BinaryExpr < ExprT1, ExprT2, std::multiplies<double> >
  55. operator * ( ExprT1 e1, ExprT2 e2 )
  56. {
  57. return BinaryExpr<ExprT1, ExprT2, std::multiplies<double> >(e1,e2);
  58. }
  59.  
  60. // Using tempates iterpreter ( с возможностью повторного использования выражения )
  61. void someFunctionMultiple ( double x )
  62. {
  63. Variable v = x; // #01
  64. BinaryExpr< BinaryExpr < Variable, int, std::plus<double> >,
  65. double,
  66. std::multiplies<double> >
  67. expr = (v + 2) * 3.0;
  68. std::cout << expr.eval() << std::endl;
  69. }
  70.  
  71. // Using tempates iterpreter
  72. void someFunctionSingle ( double x )
  73. {
  74. Variable v = x;
  75. std::cout << ((v+2)*3.0).eval() << std::endl;
  76. }
  77.  
  78.  
  79. // User friendly variant:
  80. template <class ExprT> double eval(ExprT e) { return e.eval(); }
  81.  
  82. void someSimplyFunction ( double x )
  83. {
  84. Variable v = x;
  85. std::cout << eval( (v+2)*3.0 )<< std::endl; // #02
  86. }
  87.  
  88.  
  89. int main()
  90. {
  91. // (x+2)*3 where PI = π
  92. someFunctionSingle(3.14d);
  93. someSimplyFunction(3.14d);
  94. return 0;
  95. }

Комментарии к реализации:
#01 - работает конструктор преобразоания.
#02 - сначала вычисляется выражение (v+2), имеющее тип Variable,int,plus<double>, далее вычилится выражение BinaryExpr<BinaryExpr,double,multiplies<double>>, если полностью писать "разворот" - то он будет тем же, что и тот что под строкой #01, и именно к объекту выражения этого типа и обратится конструкция e.eval().

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

Например это нужно для вычисления интеграла с помощью следующей аппроксимирующей функции:

  1. template <class ExprT>
  2. double integrate ( ExprT e, double from, double to, size_t n )
  3. {
  4. double sum=0, step=(to-from)/n;
  5. for ( double i=from+step/2; i<to; i+=step )
  6. sum+=e.eval(i);
  7. return step*sum;
  8. }

В которую передавалось бы такое выражение:

  1. Variable<double> v;
  2. // Как видно Variable теперь является шаблоном, можно было реализовать и
  3. // как обычный класс, но в первоисточнике реализовано в виде шаблонов.
  4. // Поэтому и я делаю в шаблонах.
  5. std::cout << integrate ( v/(1.0+v), 1.0, 5.0, 10) << std::endl;

Для реализации этого, решение выше требует небольших изменений. А именно - Variable теперь не будет иметь конструктора, его функционал можно реализовать как возвращение копии переменной переданной по значению в параметр. Т.к. методы должны иметь одинаковую сигнатуру - то и Literal должен иметь входящий параметр, но при этом возвращать сохраненную копию значения. Нетерминальные выражения будут передавать аргумент своим подвыражениям.

Так же придется немного изменить someFunctionSingle, а вспомогательную функцию eval(ExprT e) заменить на вспомогательный шаблонный функтор.

Код (с сокращением некоторых участков для краткости) :

  1. class Literal
  2. {
  3. public:
  4. Literal ( const double v ) : _val(v) {}
  5. double eval(double) const { return _val; }
  6. private:
  7. const double _val;
  8. };
  9.  
  10. template <class T> class Variable
  11. {
  12. public:
  13. double eval(T _val) const { return _val; }
  14. };
  15.  
  16. template <class ExprT1, class ExprT2, class BinOp> class BinaryExpr
  17. {
  18. public:
  19. // ... тот же конструктор
  20. double eval(double d) const
  21. { return _op ( _expr1.eval(d),_expr2.eval(d) ); }
  22. typedef BinaryExpr<ExprT1,ExprT2,BinOp> type;
  23. private:
  24. // ... те же поля
  25. };
  26.  
  27. template <class ExprT>
  28. double integrate ( ExprT e, double from, double to, size_t n )
  29. {
  30. Variable<double> _n;
  31. double sum=0, step=(to-from)/n;
  32. for ( double i=from+step/2; i<to; i+=step )
  33. sum+=e.eval(i);
  34. return step*sum;
  35. }
  36.  
  37. void someIntegral (void)
  38. {
  39. Variable<double> v;
  40. std::cout << integrate ( v/(1.0+v), 1.0, 5.0, 10) << std::endl;
  41. }
  42.  
  43. template <class T> void someFunctionSingle ( T x )
  44. {
  45. Variable<T> v;
  46. std::cout << ((v+2)*3.0).eval(x) << std::endl;
  47. }
  48.  
  49. template <class T> class Eval
  50. {
  51. public:
  52. Eval ( T & x ): _val(x) { }
  53. template <class ExprT> T operator () (ExprT e) { return e.eval(_val); }
  54. private:
  55. T & _val;
  56. };
  57.  
  58. template <class T> void someSimplyFunction ( T x )
  59. {
  60. Variable<T> v;
  61. Eval<T> eval(x);
  62. std::cout << eval( (v+2)*3.0 )<< std::endl;
  63. }

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

Можно добавить такие унарные функции как: sqrt, sqr, exp, log, и тогда будет возможно даже вычисление распределения Гаусса:

  1. double sigma=2.0, mean=5.0;
  2. const double Pi = 3.141593;
  3. std::cout << integrate(
  4. 1.0/(sqrt(2*Pi)*sigma) * exp(sqr(x-mean)/(-2*sigma*sigma)),
  5. 2.0,10.0,100) << std::endl;

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

  1. template < class ExprT > UnaryExpr < ExprT,double(*)(double) > sqrt ( ExprT e )
  2. { return UnaryExpr<ExprT,double(*)(double)>(e,::std::sqrt); }

С этими дополнениями у нас появляется мощное и высокопроизводительные решение для расчета арифметических выражений. В которое так же можно добавить и шаблоны логических операций. Если же метод eval сделать оператором вызова функции - то из объектов выражений можно получить функциональные объекты, которые можно использовать в сочетании с "алгоритмами" STL

  1. Variable<int> x;
  2. std::cout << std::count_if ( vct.begin(), vct.end(), x >= 0 && x <= 25 ) << std::endl;

Этот пример хорошо иллюстрирует читабельность шаблонов выражений при нулевой вычислительной стоимости на этапе выполнения.

Код с полным листингом из этой статьи, в том числе с поддержкой логических операций - в архиве.

Заключение

В заключение порекомендую дополнительный матриал (прежде всего самому себе :))
SFINAE — это просто
А так же 48-е правило (на 231 старание) Эффективного Си++ Мейерса - крайне интересное чтиво.

Комментарии

Аватар пользователя Илья Лесной

Здесь рассматривался алгоритм std::advance и его специализации, в которых, так сказать, ключом к специализации была иерархия классов std::iterator_tag.

В рамках интроспективного метапрограммирования интересны специализации алгоритма std::copy. Кстати, данная техника перекочевала почти без изменений исходного кода из boost.

Вкратце идея такова (код взять с сети, но но очень хорошо показывает суть, после него можно будет посмотреть на более конкретные реализации)

  1. template <typename It1, typename It2, bool b>
  2. It2 copy_impl(It1 first, It1 last, It2 out, constexpr boost::integral_constant<bool, b>&)
  3. {
  4. while ( first != last )
  5. {
  6. *out = *first;
  7. ++out; ++ first;
  8. }
  9. return out;
  10. }
  11.  
  12. template <typename T>
  13. It2 copy_impl(T * first, T * last, T * out, constexpr boost::true_type&)
  14. {
  15. memmove ( out, first, sizof(T)*(last-first) );
  16. return out + (last - first);
  17. }
  18.  
  19. template <typename It1, typename It2, bool b>
  20. It2 copy(It1 first, It1 last, It2 out)
  21. {
  22. typedef typename std::iterator_traits<It1>::value_type vt;
  23. return copy_impl ( first, last, out, boost::has_trivial_assign<vt>() );
  24. }

Здесь всё вполне обычно, кроме того что в оригинальном буст коде передача типов истинности осуществляется по ссылке. Особого смысла это не имеет, данные классы будут исполняться во время компиляции, данные классы являются тривиально копируемыми. В некоторых случаях буст подход будет быстрее СТЛевского, в некоторых медленее (повторюсь - скорость времени компиляции). В каких быстрее а в каких медленнее? Все зависит от типа, с типом бул - в СТЛ. С типом, размер которого больше размера указателя(ссылки) - Буст подход будет быстрее. К слову, Буст версия integral_constant чуть примитивнее чем СТЛевская.

Так же понятно зачем всё это. Копирование через меммув окажется намного быстрее, т.к. в отличие от memcpy - memmove действительно является оптимизированным системным сервисом копирования байтов, определенном в недрах компилятора, а не пространстве библиотечного кода.

В gcc реализация выглядит так:

  1. template<typename _II, typename _OI>
  2. inline _OI
  3. copy(_II __first, _II __last, _OI __result)
  4. {
  5. // concept requirements
  6. __glibcxx_function_requires(_InputIteratorConcept<_II>)
  7. __glibcxx_function_requires(_OutputIteratorConcept<_OI,
  8. typename iterator_traits<_II>::value_type>)
  9. __glibcxx_requires_valid_range(__first, __last);
  10.  
  11. return (std::__copy_move_a2<__is_move_iterator<_II>::__value>
  12. (std::__miter_base(__first), std::__miter_base(__last),
  13. __result));
  14. }

Давайте разберём это. Самое интересное оставим на потом, а начнем с ретурна. Радует что увидел, некогда любимый мной стиль ретурна со скобочками.

В нём вызывается std::__copy_move_a2. _а2 версии, это набор SFINAE перегрузок, не интересный для рассмотрения, и и служащий некоторым "диспетчером" выбора версии copy например для копирования файловый поток, или же копирования в стандартный поток, нас же интересует специализация, которая работает с общим случаем - копирования в объект, представляющий собой или указатель или же другой объект, с определенным над ним множеством операций, соответствующих указателю.

Перед передачей в нужную версию, видим проверку является ли входной итератор итератором перемещения std::move_iterator, это адаптер итератора, появившийся в C++11, и позволяющий использовать семантику перемещающего копирования из итераторов (по сути этот адаптер ничего не особого не представляет, кроме как оборачивает в правосторонне-сылочный тип возвращаемые при разыменовании итератора величины, и если над типом определена перемещающая семантика, компилятор выберет именно её и будет совершен "идеальное перемещение"(perfect forwarding), если же не определено - выберется обычное копирующее присваивание. Как воспользоваться перемещающим копированием итераторов - рассказывается здесь.

Вот так выглядит проверка:

  1. // общий шаблон
  2. template<typename _Tp>
  3. struct __is_move_iterator
  4. {
  5. enum { __value = 0 };
  6. typedef __false_type __type;
  7. };
  8.  
  9. // объявление типа, опеределенного в другом файле
  10. template<typename _Iterator>
  11. class move_iterator;
  12.  
  13. // простая специализация, подстановки конкретного типа
  14. template<typename _Iterator>
  15. struct __is_move_iterator< move_iterator<_Iterator> >
  16. {
  17. enum { __value = 1 };
  18. typedef __true_type __type;
  19. };

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

В любом из случаев, функция __copy_move_a2 перенаправит нас сюда:

  1. template<bool _IsMove, typename _II, typename _OI>
  2. inline _OI
  3. __copy_move_a(_II __first, _II __last, _OI __result)
  4. {
  5. typedef typename iterator_traits<_II>::value_type _ValueTypeI;
  6. typedef typename iterator_traits<_OI>::value_type _ValueTypeO;
  7. typedef typename iterator_traits<_II>::iterator_category _Category;
  8. const bool __simple = (__is_trivial(_ValueTypeI)
  9. && __is_pointer<_II>::__value
  10. && __is_pointer<_OI>::__value
  11. && __are_same<_ValueTypeI, _ValueTypeO>::__value);
  12.  
  13. return std::__copy_move<_IsMove, __simple,
  14. _Category>::__copy_m(__first, __last, __result);
  15. }

Здесь все понятно и нового мы не видим, проверяется что OI (выходной итератор) указывает на тот же тип что и II(входной итератор). Некоторая путаница может возникнуть с направлениями. Подробнее о них можно узнать здесь, в кратце же так же как и с файловыми потоками - выходной - это как бы то, что выводит нечто нам на запись, т.е. куда мы можем писать. А входной - это то откуда мы можем читать.

Ну и самое главное - это проверка на тривиальность, далее, в случае тривиальности типов в итераторах (__simple==true), управление передаётся статическому методу, структуры-обёртки, который делает несколько небесполезных проверок:

  1. template<bool _IsMove>
  2. struct __copy_move<_IsMove, true, random_access_iterator_tag>
  3. {
  4. template<typename _Tp>
  5. static _Tp*
  6. __copy_m(const _Tp* __first, const _Tp* __last, _Tp* __result)
  7. {
  8. #if __cplusplus >= 201103L
  9. // trivial types can have deleted assignment
  10. static_assert( is_copy_assignable<_Tp>::value,
  11. "type is not assignable" );
  12. #endif
  13. const ptrdiff_t _Num = __last - __first;
  14. if (_Num)
  15. __builtin_memmove(__result, __first, sizeof(_Tp) * _Num);
  16. return __result + _Num;
  17. }
  18. };

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

  1. class TrivialClass
  2. {
  3. private:
  4. int a_;
  5. char b_;
  6. public:
  7. // закрываем конструктор копирования
  8. TrivialClass(const TrivialClass&) = delete;
  9. TrivialClass& operator=(const TrivialClass&) = delete;
  10.  
  11. // объяление данного конструктора "перекроет"
  12. // конеструктор по-умолчанию, поэтому нужно объявить его
  13. TrivialClass() = default;
  14.  
  15. TrivialClass(int a, char b):a_(a),b_(b){}
  16.  
  17. };
  18.  
  19. int main(void)
  20. {
  21. TrivialClass t1;
  22. TrivialClass t2;
  23. t1=t2; //compile error
  24. return 0;
  25. }

Ну и конечно же проверка на то, чтобы разность между указателями существовала. Ну а далее запускается нативный сервис, memmove.
На случай же нетривиальных типов будут выбраны специализации __copy_m, почти полностью идентичиные примеру из начала данного комментария, с всего одной разницой:

  1. *__result = *__first;
  2. // vs
  3. *__result = std::move(*__first);

Рассмотрим, как происходит работа в is_copy_assignable. Прежде всего мы увидим необычный decltype, и этот деклтайп поможет нам узать новое, но об этом чуть позже.

  1. template<typename _Tp, typename _Up>
  2. class __is_assignable_helper
  3. : public __sfinae_types // в этом классе просто определены типы __one и __two
  4. {
  5. template<typename _Tp1, typename _Up1>
  6. static decltype(declval<_Tp1>() = declval<_Up1>(), __one())
  7. __test(int);
  8.  
  9. template<typename, typename>
  10. static __two __test(...);
  11.  
  12. public:
  13. static constexpr bool value = sizeof(__test<_Tp, _Up>(0)) == 1;
  14. };
  15.  
  16. // is_assignable (будет являться типом истины или лжи)
  17. template<typename _Tp, typename _Up>
  18. struct is_assignable
  19. : public integral_constant<bool,
  20. __is_assignable_helper<_Tp, _Up>::value>
  21. { };

На основе этого is_assignable определено много других, например на возможность перемещающего копирования, или присваивающего, которое мы и рассмотрим (остальное же можно рассмотреть в файле local_type_traits)

Сейчас будет немного необычного, как и обещал :)

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

  1. // общий тип, прежде всего нацелен на вход_проверку на void, это уже немного необычно
  2. template<typename _Tp, bool = is_void<_Tp>::value>
  3. struct __is_copy_assignable_impl{};
  4. // интересный факт, пустую пару фигурных скобок здесь можно опустить
  5. // т.к. данный шаблон никогда не будет актуализирован. Компилятор, в случае
  6. // is_void<_Tp> == true перенаправит в специализацию ниже, как в
  7. // более предпочтительный варант:
  8.  
  9. template<typename _Tp>
  10. struct __is_copy_assignable_impl<_Tp, true>
  11. : public false_type { };
  12. // логично, void = void (не для нетиповых указателей) а для пустого типа, не определено.
  13.  
  14. // Если же не void то выбирается false специазация шаблона, которая
  15. // наследована от is_assignable, и которая опять через наследование приведет нас
  16. // к типу истинности или лжи, хотя правильнее говорить к integral_constant, но
  17. // актуализированного так, что будет соответствовать одному из этих типов.
  18. template<typename _Tp>
  19. struct __is_copy_assignable_impl<_Tp, false>
  20. : public is_assignable<_Tp&, const _Tp&>
  21. { };
  22.  
  23. // is_copy_assignable
  24. template<typename _Tp>
  25. struct is_copy_assignable
  26. : public __is_copy_assignable_impl<_Tp>
  27. { };

Вот такой вот нерассмотренный ранее способ отнаследовать "метаистину" или "металожь", но не совсем прямо.

Теперь о decltype

  1. static decltype(declval<_Tp1>() = declval<_Up1>(), __one())
  2. __test(int);

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

Обратим внимение, что в is_assignable<_Tp&, const _Tp&> указан тип-ссылка. Здесь, в отличие от начала данного комментария, применение ссылок очень важно.

Рекомендую посмотреть на шаблон __is_convertible_helper в начале этой темы. И на шаблон is_assignable, в который передаются ссылки. Все потому что внутри шаблонов, если тип "чистый" - то он является правостороннем выражением (не ссылкой, а выражением), который можно присваивать, но не к которому можно присваивать.

Например:

  1. short()=float(); // error: lvalue required as left operand of assignment

Но, если используя возможности шаблонов мы преобразуем (в данном случае просто добавив символ, пользуясь тем, что это время компиляции и не нужно преобразовывать реальные объекты, каr например это делает std::move (через касты), а манипулировать только их типами), тип являющийся rvalue в lvalue ссылку. Вот такая вот магия. short&=float& уже возможно.

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

  1. template <class T> void __foo_aux(T){};
  2. __foo_aux<short>(float()); //ok

Вывод: Компилятор различает присваивание и передачу параметра в функцию по значению, хотя при этом будут задействованы те же механизмы копирования, что и при присваивании.

Кстати, в gcc местами содержится избыточный дубляж кода. Например, тех же мататипов истинности и лжи несколько. Возможно это наследие, которое потом структурируется в нечто единое, возможно для некоторой совместимости. Но благодаря этому мы узнали что есть деклатайп с вариативным количеством выражений, и его можно использовать в клиентском коде. Кстати рассмотренная реализация лежит в директории libitm, в дриректории же libstdc++-v3, лежит так сказать более осовременная версия базового меташаблона.

  1. template<typename _Tp, typename _Up>
  2. class __is_assignable_helper
  3. {
  4. template<typename _Tp1, typename _Up1,
  5. typename = decltype(declval<_Tp1>() = declval<_Up1>())>
  6. static true_type
  7. __test(int);
  8.  
  9. template<typename, typename>
  10. static false_type
  11. __test(...);
  12.  
  13. public:
  14. typedef decltype(__test<_Tp, _Up>(0)) type;
  15. };

В общем читая STL можно наблюдать что нет единой стилистики. Например игра с типами, могут быть typedef struct { char array[2]; } no а потом заменить на typedef char no[2];

  1.  

На последок ещё несколько простых, но небезынтересных примера:
проверка на наличия деструктора в типе:

  1. template<typename _Tp, typename = decltype(declval<_Tp&>().~_Tp())>
  2. static true_type __test(int);
  3.  
  4. template<typename> static false_type __test(...);
  5. // ... дальше всё как обычно