Выбрать главу

В строке 1 объявлен шаблон класса, в строке 2 объявлен шаблон функции. В строке 3 производится явное инстанциирование шаблона класса, типами параметров выступают int и числовое значение. В строке 4 производится неявное инстанциирование шаблона функции, тип параметра шаблона здесь будет int, который выводится из типа входного аргумента. В строке 5 производится явное инстанциирование; оно здесь необходимо, потому что из типов входных аргументов нельзя однозначно определить, какой тип параметра должен использоваться в шаблоне.

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

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

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

4.2. Синхронные вызовы

4.2.1. Инициатор

Проанализируем различные реализации инициатора синхронных вызовов (Листинг 24):

Листинг 24. Реализации инициатора для синхронных вызовов

class Executor

{

public:

  void callbackHandler(int eventID);

  void operator() (int eventID);

};

using ptr_callback = void(*) (int, void*);

using ptr_callback_static = void(*) (int, Executor*);

using ptr_callback_method = void(Executor::*)(int);

void run(ptr_callback ptrCallback, void* contextData = nullptr)  // (1)

{

  int eventID = 0;

  ptrCallback(eventID, contextData);

}

void run(ptr_callback_static ptrCallback, Executor* contextData = nullptr)  // (2)

{

  int eventID = 0;

  ptrCallback(eventID, contextData);

}

void run(Executor* ptrClientCallbackClass, ptr_callback_method ptrClientCallbackMethod)  // (3)

{

  int eventID = 0;

  (ptrClientCallbackClass->*ptrClientCallbackMethod)(eventID);

}

void run(Executor callbackHandler)  // (4)

{

  int eventID = 0;

  callbackHandler(eventID);

}

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

Листинг 25. Шаблон для инициатора синхронного вызова

template <typename CallbackArgument>

void run(CallbackArgument callbackHandler)

{

  int eventID = 0;

  //Some actions

  callbackHandler(eventID);

}

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

Что же нам делать для остальных реализаций? Для указателей на функцию и указателей на статический метод (строки 1 и 2) можно сделать отдельный шаблон с двумя параметрами (Листинг 26):

Листинг 26. Шаблон для инициатора с двумя параметрами

template <typename CallbackArgument, typename Context>

void run(CallbackArgument callbackHandler, Context* context)

{

  int eventID = 0;

  //Some actions

  callbackHandler(eventID, context);

}

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

вернуться

17

Для изучения можно порекомендовать книгу «Вандевурд, Джосаттис, Грегор. Шаблоны C++: справочник разработчика», где подробно рассматриваются соответствующие темы.