Обобщенное программирование - это подход к программированию, когда алгоритм пишется без указания конкретных типов данных. Используя данный подход можно значительно увеличить количество повторно используемого кода. В C++ данный подход реализуется за счет механизма шаблонов. В данном докладе рассмотрим некоторые возможности по обобщенному программированию, которые предоставляет C++. На конкретных примерах рассмотрим, как они могут упростить нам жизнь и с какими трудностями приходится сталкиваться при их использовании.
12. Вывод типов
12
template<typename T>
void f(ParamType param);
f(expr);
// Example:
template<typename T>
void f(const T& param);
int x = 0;
f(x); // T is int, ParamType is const int&
13. PyramType - не ссылка и не указатель
13
template<typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T is int, ParamType is int
f(cx); // T is int, ParamType is int
f(rx); // T is int, ParamType is int
14. ParamType - ссылка
14
template<typename T>
void f(T& param); // param is a reference
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T is int, ParamType is int&
f(cx); // T is const int, ParamType is const int&
f(rx); // T is const int, ParamType is const int&
15. ParamType - константная ссылка
15
template<typename T>
void f(const T& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T is int, ParamType is const int&
f(cx); // T is int, ParamType is const int&
f(rx); // T is int, ParamType is const int&
16. ParamType - указатель
16
template<typename T>
void f(T* param);
int x = 27;
const int *px = &x;
f(&x); // T is int, ParamType is int*
f(px); // T is const int, ParamType const int*
17. ParamType - универсальная ссылка
17
template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27;
const int cx = x;
const int& rx = x;
// l-value examples
f(x); // T is int&, ParamType is int&
f(cx); // T is const int&, ParamType is const int&
f(rx); // T is const int&, ParamType is const int&
// r-value example
f(27); // T is int, ParamType is int&&
18. ParamType - массив
18
const char name[] = "J. P. Briggs";
template<typename T>
void f(T param);
f(name); // T is const char*, ParamType is const char*
template<typename T>
void f(T& param);
f(name); // T is const char [13], ParamType is const char (&)[13]
// Compile time array size
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
19. ParamType - функция
19
void someFunc(int, double);
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc); // T is void (*)(int, double), ParamType is void (*)(int, double)
f2(someFunc); // T is void (&)(int, double), ParamType is void (&)(int, double)
27. Variadic Templates
Разворачивание шаблоно! может иметь место только в определенных
контекстах:
Список аргументов функции: f(&args...);
Список аргументов шаблона: container<A,B,C...> t1;
Список параметров функции: template<typename ...Ts> void f(Ts...) {}
Список параметров шаблона: template<T... Values>
Базовые классы и список инициализации класса: class X : public Mixins...
Список инициализации: int res[sizeof...(args) + 2] = {1,args...,2}; 27
36. Применение функции к Tuple
36
template<typename F, typename Tuple, int... N>
auto call(F f, Tuple &&t) {
return f(std::get<N>(t)...);
}
int sum(int a, int b, int c) {
return a + b + c;
}
td::tuple<int, int, int> args(1, 2, 3);
call<int(&)(int,int,int), std::tuple<int,int,int>&, 0, 1, 2>(sum, args);
37. Применение функции к Tuple (2)
37
template<typename F, typename Tuple, bool Enough, int TotalArgs, int... N>
struct call_impl {
auto static call(F f, Tuple&& t) {
return call_impl<F, Tuple, TotalArgs == 1 + sizeof...(N),
TotalArgs, N..., sizeof...(N)
>::call(f, std::forward<Tuple>(t));
}
};
38. Применение функции к Tuple (3)
38
template<typename F, typename Tuple, int TotalArgs, int... N>
struct call_impl<F, Tuple, true, TotalArgs, N...> {
auto static call(F f, Tuple&& t) {
return f(std::get<N>(std::forward<Tuple>(t))...);
}
};
39. Применение функции к Tuple (4)
39
template<typename F, typename Tuple>
auto call(F f, Tuple&& t) {
typedef typename std::decay<Tuple>::type type;
return call_impl<F, Tuple, 0 == std::tuple_size<type>::value,
std::tuple_size<type>::value
>::call(f, std::forward<Tuple>(t));
}
call(sum, args);
40. PRC на основе boost.asio и boost.serialization
40
https://github.com/gomons/CppRpcLight
42. Service.cpp (Выполняется на сервере)
42
RPC_DECLARE(sum, int, int a, int b) {
return a + b;
}
RPC_DECLARE(echo, std::string, std::string str) {
return str;
}
45. Вызов удаленной функции
45
auto sum_res = sum(client_сonnection, 5, 6);
auto sum_future = sum_async(client_сonnection, 5, 6);
auto sum_res2 = sum_future.get();
auto echo_res = echo(client_сonnection, std::string("Ping!"));
auto echo_future = echo_async(client_сonnection, std::string("Ping!"));
auto echo_res2 = echo_future.get();
46. Немного о страданиях
Разные компиляторы поддерживают немного разный синтаксис
Трудно найти проблему по диагностическому сообщению
Только новые компиляторы
Сложно писать, поддерживать, разбираться
Долгая компиляция, большой размер бинарника
Код шаблонов должне находиться в заголовочном файле
46
48. Ссылки
Scott Meyers - Effective Modern C++
Stroustrup - A Tour of C++ - 2013
Sutter, Alexandrescu - C++ Coding Standart
https://m.habrahabr.ru/post/54762/
https://habrahabr.ru/post/166849/
https://habrahabr.ru/post/228031/
https://habrahabr.ru/post/245719/
48
Editor's Notes
Обобщенное программирование - это подход, при котором при написании алгоритмов используются не конкретные типы данных, а некоторые их абстрактные описания. Как следствие, алгоритм может работать с любыми типами данных, которые поддерживают данное описание. В результате вместо программы у нас получается шаблон программы, в который можно подставлять разные типы. В C++ основным средством поддержания обобщенного программирования являются шаблоны или темплейты.
Программа на C++, которая содержит шаблоны - это уже не просто программа, а метапрограмма. Используя шаблонный код и конкретные значения параметров этого шаблонного кода, компилятор генерирует программу, которая потом идет на компиляцию.
Превосходным примером эффективности и красоты обобщенного программирования является библиотека STL, написанная Александром Степановым при поддержке Меган Ли. Эти люди стоят у истоков обобщенного программирования в C++. В 1992 году они занимались исследованиями в области обобщенного программирования. Вопрос состоял в том, чтобы проверить, можно ли писать программы максимально обобщенным способом, чтобы при этом они не теряли своей эффективности. Оказалось, что это возможно.
Также стоит отметить работы Андрея Александреску. В его книге можно найти очень интересный подход к обобщенному программированию. Его библиотека Loki - это своеобразная мета-библиотека для C++, которая позволяет сгенерировать библиотеку для конкретных нужд за счет настраивания через специальные классы-стратегии.
В C++ шаблоном может быть класс либо функция. Базовый синтаксис шаблонов довольно прост. Перед классом или функцией пишется ключевое слово template, после которого в угловых скобках перечисляются аргументы шаблона. В качестве аргументов шаблона могут выступать типы, значения, а также другие шаблоны. В определении класса или функции в качестве типов и констант можно использовать параметры шаблона. На слайде приведет простой класс статического массива фиксированного размера. Преимуществом данного подхода по сравнению с обычными статическими массивами является то, что в методах доступа к элементам массива можно добавить проверки, чтобы не допустить выход за границы массива. В стандартной библиотеке уже есть такой класс, это std::array.
До появления шаблонов, в C++ был механизм метапрограммирования - это препроцессор Си. На слайде показан пример, аналогичный рассмотренному ранее, но без использования шаблонов. К недостаткам данного метода можно отнести то, что каждый новый тип нужно объявлять вручную. Но основным недостатком данного подхода является обильное использование макросов, что делает программы слишком сложными для отладки.
В разных языках обобщенное программирование поддерживается по-разному и выполняет разные цели. Например, в Java есть дженерики, которые работают через механизм type erasure и фактически используются только для усиления типизации языка. Обобщенные контейнеры в Java были и до появления дженериков, и работали через общий базовый класс. Похожая ситуация обстоит и с C.
В этих языка используется похожий с C++ синтаксис, но способ работы данного механизма в этих языках абсолютно отличаются. В C++ шаблоны - это средство генерации кода. По описанию программы компилятор генерирует нужный нам код. Как следствие получается программа, которая не требует дополнительных ресурсов во время выполнения. В Java и C# это не так, там дженерики - это механизм времени выполнения, который не позволяет генерировать новый код.
Без понимани правил вывода типов C++, практически невозможно писать сложных программ с использованием шаблонов. На слайде представлены элементы, которые будут использоваться для объяснения правил вывода типов. В ходе процедуры вывода типов вычисляется значение 2-х типов: для T и для ParamType. Как правило, эти типы отличаются. ParamType часто может иметь квалификаторы const, может быть типом ссылки или указателя. Тип, который выводится для T, зависит от того, какие к квалификаторы используются для типа ParamType. Условно можно выделить 3 случая:
ParamType - ссылка или указатель
ParamType - универсальная ссылка
ParamType - не ссылка и не указатель
В данном случае при выводе типа T, квалификаторы const, volotile агрумента, а также его ссылочность игнорирутся. T и PatamType имеют одинаковый тип.
Рассмотрим случай, когда ParamType - это ссылка или указатель. В данном случае правила вывода типа работают следующим образом: если тип аргумента - ссылка, то она игнорируется. Тут мы видим, что если в качестве аргумента шаблонной функции передается константынй тип, то константность становится частью шаблонного типа T. В случае передачи в функцию ссылочного типа rx, тип T игнорирует ссылочность.
Если мы будем передавать в функцию значение по константной ссылке, то константность перестанет быть частью типа T. В остальном все остантеся, как прежде.
Если ParamType является указателем, то все происходит с точностью, как и с ссылками. Параметр T игнорирует то, что тип аргумента функции - ссылка. Далее вывод типов происходит по тем же правилам, как рассказывалось раньше.
С C++11 появились rvalue-reference. При выводе типов шаблона для них работают отдельные правила. Типы выводятся по-разному в зависимости от того, передаем мы в функцию l-value или r-value. При передаче l-value, T и ParamType имеют одинаковые типы, при передаче r-value, T игнорирует ссылочность типа, а ParamType имеет r-value тип.
Для массивов и функций при выводе типов работают отдельные правила. Они одинаковые для массивов и функций. Особенностью передачи массивов по значению является то, что при этом происходит передача указателя на первый элемент массива, т. е. тип массива теряется. Однако как массив, так и функцию можно передавать по ссылке, тогда их типы сохраняются. Это дает возможность, например, определять размер массива во время компиляции.
Как упоминалось выше, тут работаю все те же самые правила, что и с массивами.
В
В мире C++ принято различать шаблонную функцию и функцию шаблона. Под шаблонной функцией обычно понимают то, что пишется после ключивого слово template. Функция шаблоно - это конкретный экземпляр шаблонной функции (шаблоны в C++ - средство генерации кода). Очень часто эти понятия использут взаимозаменяемо.
Шаблонный класс сам по себе не является типом, функцией или объектом. Чтобы сгенерировать код из шаблоноа, его нужно инстанцировать. Инстанцирование может быть явным и неявным. Неявное инстанцирование происходит в местах, где код ссылается на шаблон в контексте, где требуется его определение и при этом до этого не было произведено явное инстанцирование.
Концепты (concepts) - требования и ограничения, накладываемые на параметры шаблона. C++ не поддерживает концептов. Но в некотором виде это можно компенсировать различными приемами программирования. На слайде представлен способ проверки наличия у шаблонного типа матода определенной сигнатуры: определенного возвращаемого типа и определенных значений параметров. Если шаблонный параметр не будет иметь функцию такой сигнатуры, произойдет ошибка компиляции.
Специализация - это механизм, который позволяет предоставить реализацию шаблона для конкретного типа. Специализация может быть полной, а может быть частичной. Функции не поддерживают частичную специализацию, а классы - поддерживают. Частично специализированный шаблон - это новый шаблон, который может иметь свой список параметров.
Характеристики - это метод обобщенного программирования, который позволяет принимать решения во время компиляции. Работа данного механизма основана на частичной специализации.
Вариадики - мощнейший и долгожеланный механизм, который позволяет избавиться от огромнейшего количества хаков, которые раньше применялись для их имитации. Да, вариадики раньше применялись, на чаще всего для них были установдены ограничения, предусмотренные автором библиотеки. Например, в некоторых реализациях кортеджей максимальное количество элементов не могло превышать 10. 10 - это большое число для картеджа, и все же иногда его не хватало. В приходом вариадиков эти ограничения остались в прошлом. Вариадики позволяют писать программы времени компиляции в функциональном стили, где последовательность параметров разделяется на голову и хвост. Голова списка обрабатывается, а для хвоста списка вызывается рекурсивная обработка. Нужно понимать, что эта рекурсивная обработка выполняется компилятором, что может сильно замедлить время его работы, а также она имеет некоторые ограничения. Да, это не число 10, однако не стоит сильно увлекаться.
Ложкой дегдя в этом всем является то, что вариадики могут раскрываться только в нескольких контекстах.
Constexpr позволяет указать, какие вычисления должны выполняться во время компиляции. Такие выражения затем можно использовать в контекстах, где разрешается использовать только константы. Использование constexpr при объявлении объектов подразумевает, что они будут константными, а при исползовании для объявления функций, что они будут инлайниться. Однако при этом на constexpr выражения накладываются довольно жесткие ограничения. В частности, такие функции не должны быть вируальными, должны возрващать литеральный тип, в качестве аргументов принимают только литеральные типы.
Constexpr выражения позволяют писать более крутые метапрограммы. Например, можно обфрусцировать строки во время компиляции. В данном случае мы объявляем функцию sizeCalculate, которая выполняется во время компиляции и преобразует строку, в данном случае добавляет к каждому симполу 1. Но с таким подходом есть, как минимум, 2 проблемы: далеко не все компиляторы это скомпилируют, а если и скомпилируют, то оптимизатор может убрать все вычисления времени компиляции и заменить строки константами, что сведет на нет наши старания.
Теперь вернемся к практическому аспекту обобщенного программирования. Ранее упоминался такой тип, как картедж. В C++11 его реализация стала довольно понятной. Кортедже очень широко распространены в функциональном программировании. Далее мы увидим, что некоторые аспекты фукнционального программирования также применимы в C++. При работе с вариадиками, особое значение имеет рекурсия. Как правило, списко типов разделяется на голову и тело. Голова обрабатывается, а для тела рекурсивно вызывается функция. При это нужно не забыть определить окончание рекурсии, иначе программа не скомпилируется.
В принцепе tuple готов к использованию. Однака данный вариант очень неудобен в использовании. Чтобы это исправить, обычно пишут функции доступа к элементам tuple.
Большинство продемонстрированных примеров были взяты из интернета. Это был готовый и отлаженный код, в котором были предусмотрены многие нюансы. Однако если приходится написать что-то новое, то, как правило, это становится настоящей головоломкой. И все бы ничего, но компилятор, который должен нам помогать, часто делает нашу жизнь невыносимой. Синтаксис шаблонов достаточно трудный, и разные компиляторы реализуют его со своими особенностями, некоторые из которых сложились исторически. Например, Visual Studio позволят скомпилировать код, который является неправильным по стандарту, что приводит к тому, что он не компилируется на GCC. При этом clang, при работе под Windows, пытается воспроизвести все послабления стандарта Visual Studio, но делает это не до конца. При этом диагностические сообщения выглядят так, что в них страшно смотреть.
К сожалению, большинство проектов использует C++98, в котором возможности обобщенного программирования сильно ограничены или не такие удобные, как в C++11.
Качественной шаблонной библиотекой очень удобно пользоваться, но нужно быть настоящим гуру, чтобы ее написать. Разбираться в программах с обильным использованием шаблонов тоже очень тяжело. Поэтому перед тем, как начинать активно везде использовать шаблоны, нужно провести тчательное исследование, на каких платформах и с какими людьми придется в будущем работать.
Не смотря на то, что шаблоны - очень мощный механизм, им очень легко начать злоупотреблять. Поэтому при их использовании всегда нужно иметь в виду, что вы можете сделать жизнь других разработчиков невыносимой.