Kostenlos

Обратные вызовы в C++

Text
Als gelesen kennzeichnen
Schriftart:Kleiner AaGrößer Aa

4.3.2. Параметризация типов

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

Поясним сказанное на примере. Предположим, мы реализовали алгоритм сортировки пузырьком (Листинг 31).

Листинг 31. Сортировка массива методом пузырька

void sort_bubble(int* data, size_t size)

{

  for (size_t i = 0; i < size – 1; i++)

  {

    for (size_t j = 0; j < size – i – 1; j++)

    {

      if (data[j + 1] < data[j])

      {

        int temp = data[j];

        data[j] = data[j + 1];

        data[j + 1] = temp;

      }

    }

  }

}

Описанный код работает с числами. Параметризуем типы (Листинг 32):

Листинг 32. Параметризация типов для сортировки пузырьком

template <typename Data>                   // (1)

void sort_bubble(Data* data, size_t size)  // (2)

{

  for (size_t i = 0; i < size – 1; i++)

  {

    for (size_t j = 0; j < size – i – 1; j++)

    {

      if (data[j + 1] < data[j])

      {

        Data temp = data[j]; // (3)

        data[j] = data[j + 1];

        data[j + 1] = temp;

      }

    }

  }

}

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

4.3.3. Объявление предикатов

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

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

Объявим предикат как дополнительный параметр шаблона (Листинг 33).

Листинг 33. Шаблон с объявлением предиката

template <typename Data, typename Predicate>              // (1)

void sort_bubble(Data* data, size_t size, Predicate less) // (2)

{

  for (size_t i = 0; i < size – 1; i++)

  {

    for (size_t j = 0; j < size – i – 1; j++)

    {

      if (less (data[j + 1], data[j])) // (3)

      {

        Data temp = data[j];

        data[j] = data[j + 1];

        data[j + 1] = temp;

      }

    }

  }

}

По сравнению с предыдущим кодом из Листинг 32 изменения здесь следующие: в объявлении шаблона (строка 1) объявлен дополнительный параметр – предикат, в функции шаблона (строка 2) предикат объявляется как дополнительный входной параметр, в строке 3 вместо операции сравнения происходит вычисление предиката.

В качестве предикатов могут использоваться:

• глобальные функции;

• статические функции класса;

• перегруженные операторы;

• лямбда-выражения.

В Листинг 34 продемонстрировано использование предикатов различных типов.

Листинг 34. Сортировка данных с использованием предикатов различных типов

struct DBRecord  // (1)

{

  char firstName[50];

  char lastName[50];

};

bool CompareByFirstName(const DBRecord& rec1, const DBRecord& rec2)  // (2)

{

  return strcmp(rec1.firstName, rec2.firstName) < 0;

}

bool CompareByLastName(const DBRecord& rec1, const DBRecord& rec2)  // (3)

{

  return strcmp(rec1.lastName, rec2.lastName) < 0;

}

class SortRules  // (4)

{

public:

  enum {SORT_ASC = 1, SORT_DESC = 2} sortDirect;             // (5)

  enum { SORT_FIRST_NAME = 1, SORT_LAST_NAME = 2 } sortWhat; // (6)

  bool operator () (const DBRecord& rec1, const DBRecord& rec2) const // (7)

  {

    if (sortDirect == SORT_ASC)

    {

      if (sortWhat == SORT_FIRST_NAME)

      {

        return strcmp(rec1.firstName, rec2.firstName) < 0;

      }

    else

    {

      return strcmp(rec1.lastName, rec2.lastName) < 0;

    }

    }

    else

    {

      if (sortWhat == SORT_FIRST_NAME)

      {

        return strcmp(rec1.firstName, rec2.firstName) > 0;

      }

      else

      {

        return strcmp(rec1.lastName, rec2.lastName) > 0;

      }

    }

  }

};

int main()

{

DBRecord dbRecArray[10];  // (8)

//Read from database

sort_bubble(dbRecArray, 10, CompareByFirstName); // (9)

sort_bubble(dbRecArray, 10, CompareByLastName);  // (10)

sort_bubble(dbRecArray, 10, [](const DBRecord& rec1, const DBRecord& rec2)  // (11)

{

return strcmp(rec1.firstName, rec2.firstName) < 0;

});

sort_bubble(dbRecArray, 10, [](const DBRecord& rec1, const DBRecord& rec2)  // (12)

{

return strcmp(rec1.lastName, rec2.lastName) < 0;

});

SortRules rules; // (13)

rules.sortWhat = SortRules::SORT_LAST_NAME;  // (14)

rules.sortDirect = SortRules::SORT_ASC;      // (15)

sort_bubble(dbRecArray, 10, rules);          // (16)

}

В строке 8 объявлен массив структур, сами структуры объявлены в строке 1 (предположим, что это записи базы данных). В строке 9 и 10 происходит сортировка массива с использованием предикатов в виде внешней функции, в строках 11 и 12 – в виде лямбда-выражений.

В строке 13 объявлен предикат как экземпляр класса. Если посмотреть объявление класса (строка 4), то можно увидеть, что он позволяет осуществлять настройку правил: в строке 5 имеется переменная для настройки порядка сортировки (возрастание либо убывание), в строке 6 имеется переменная для настройки поля сортировки. В строке 7 реализован перегруженный оператор, который в соответствии с настроенными правилами вычисляет, является ли первый элемент меньше второго. В строках 14 и 15 производится настройка предиката, в строке 16 – сортировка в соответствии с заданными правилами.

4.3.4. Предикаты по умолчанию

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

Листинг 35. Шаблон с предикатом по умолчанию

template <typename Data>  // (1)

struct default_less

{

  bool operator()(const Data& x, const Data& y)  // (2)

  {

    return x < y;

  }

};

template <typename Data, typename Predicate = default_less<Data>>        // (3)

void sort_bubble(Data* data, size_t size, Predicate less = Predicate())  // (4)

{

  for (size_t i = 0; i < size – 1; i++)

  {

    for (size_t j = 0; j < size – i – 1; j++)

    {

      if (less (data[j + 1], data[j]))

 

      {

        Data temp = data[j];

        data[j] = data[j + 1];

        data[j + 1] = temp;

      }

    }

  }

}

В строке 1 объявлен шаблон для структуры, реализующей предикат сравнения. В этой структуре перегружен оператор (строка 2), который возвращает результат сравнения двух аргументов. Он будет корректно работать как для чисел, так и для объектов, в которых перегружен оператор «меньше».

В строке 3 объявлен шаблон для функции сортировки. Первый параметр шаблона – это тип данных, которые необходимо сортировать, а второй параметр – это тип предиката. По умолчанию типом предиката является структура, объявленная выше, которая инстанциируется соответствующим типом данных.

В строке 4 объявлена функция шаблона. Первый параметр здесь – это данные для сортировки, а второй параметр – предикат для вычисления меньшего элемента. Если при вызове функции предикат не задан, то в качестве значения по умолчанию будет подставлена переменная – экземпляр структуры, объявленной в строке 1. Инстанциироваться эта структура будет типом Data, переданным как первый параметр шаблона.

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

4.4. Асинхронные вызовы

4.4.1. Инициатор

Также, как мы делали при анализе синхронных вызовов, проанализируем различные реализации инициатора асинхронных вызовов (Листинг 36, некоторые фрагменты кода пропущены, чтобы не загромождать описание).

Листинг 36. Реализации инициатора асинхронных вызовов для различных типов аргументов

class Executor;

class CallbackHandler

{

public:

  void operator() (int eventID);

};

//Pointer to function

class Initiator1

{

public:

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

  void setup(ptr_callback pPtrCallback, void* pContextData) ;

private:

  ptr_callback ptrCallback = nullptr;

  void* contextData = nullptr;

};

//Pointer to the class static method

class Initiator2

{

public:

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

  void setup(ptr_callback_static pPtrCallback, Executor* pContextData) ;

private:

      ptr_callback_static ptrCallback = nullptr;

      Executor* contextData = nullptr;

};

//Pointer to the class member method

class Initiator3

{

public:

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

  void setup(Executor* argCallbackClass, ptr_ callback_method argCallbackMethod);

private:

  Executor* ptrCallbackClass = nullptr;

  ptr_ callback_method ptrCallbackMethod = nullptr;

};

//Functional object

class Initiator4

{

public:

  void setup(const CallbackHandler& callback);

private:

  CallbackHandler callbackObject;

};

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

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

template<typename CallbackArgument>

class Initiator

{

public:

  void setup(const CallbackArgument& argument)

  {

      callbackHandler = argument;

  }

  void run()

  {

    int eventID = 0;

    //Some actions

    callbackHandler(eventID);

  }

private:

  CallbackArgument callbackHandler;

};

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

4.4.2. Хранение лямбда-выражений

Почему хранение лямбда-выражений является проблемой?

При объявлении лямбда-выражения компилятор генерирует функциональный объект, который называется объект-замыкание (closure type). Этот объект хранит в себе захваченные переменные и имеет перегруженный оператор вызова функции. Сигнатура оператора повторяет сигнатуру лямбда-выражения, а в теле оператора размещается код выражения. Пример объекта-замыкания приведен в Листинг 38.

Листинг 38. Лямбда-выражение и объект-замыкание

int main()

{

  int capture = 0;

  [capture](int eventID) {/*this is a body of lambda*/};

  //The following object will be generated implicitly by the compiler from lambda declaration

  class Closure

  {

  public:

    Closure(int value) :capture(value) {}

    void operator() (int eventID)

    {

      /*this is a body of lambda*/

    }

    int capture; //captured value

  };

}

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

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

Листинг 39. Объект-замыкание с преобразованием в указатель на функцию

int main()

{

  [](int eventID) {/*this is a body of lambda*/};  // (1)

  //The following object will be generated implicitly by the compiler from lambda declaration

  class Closure  // (2)

  {

  public:

    void operator() (int eventID)  // (3)

    {

      call_invoker(eventID);

    }

    static void  call_invoker(int eventID) { /*this is a body of lambda*/ }  // (4)

    using function_pointer = void(*)(int);  // (5)

    operator function_pointer() const       // (6)

    {

      return call_invoker;

    }

  };

  //Conversion the closure object to the function pointer

  Closure cl;  // (7)

  using pointer_to_function = void(*)(int);  // (8)

  pointer_to_function fptr = cl;             // (9)

  //Conversion a lambda to the function pointer

 fptr = [](int eventID) {/*this is a body of lambda*/};  // (10)

}

В строке 1 объявлено лямбда-выражение, в строке 2 объявлен объект-замыкание. Подчеркнем: этот объект здесь всего лишь для демонстрации, чтобы показать, как он будет сгенерирован компилятором. В реальном коде такой объект объявлять не нужно, компилятор его создаст при объявлении лямбда-выражения.

В строке 3 объявлен перегруженный оператор, который вызывает статическую функцию 4. В той функции размещается код лямбда-выражения.

В строке 5 объявлен тип указателя на функцию, в строке 6 объявлен оператор преобразования типа. Реализация оператора возвращает указатель на статическую функцию 4.

В строках 7–9 показано, как осуществляется преобразование функционального объекта к указателю на функцию. В строке 7 объявлен объект-замыкание, в строке 8 объявлен тип указателя на функцию. В строке 9 объявляется переменная этого типа и вызывается перегруженный оператор присваивания 6, который возвращает указатель на функцию. Теперь в переменной fptr будет храниться указатель на статическую функцию, которая была объявлена в соответствующем функциональном объекте.

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

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

Если необходимо хранить лямбда-выражение в локальной переменной, можно использовать тип auto. Это означает, что компилятор подставит соответствующий тип, который будет сгенерирован из объявления лямбда-выражения (см. Листинг 40).

Листинг 40. Сохранение лямбда-выражения в локальной переменной

int capture = 10;

auto lambda = [capture](int eventID) {/*this is a body of lambda*/};

lambda(10); //lambda call

Однако указанный способ не будет работать, когда требуется сохранить лямбда-выражение в классе. Мы не можем объявить переменную – член класса с типом auto, потому что это означало бы объявление переменной заранее не определенного типа, что не допускается.

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

 

Добавим в реализацию инициатора, описанного в Листинг 37 п. 4.4.1, два конструктора. Один конструктор будет с переменной – аргументом обратного вызова для инициализации члена класса. Другой конструктор будет без аргументов (конструктор по умолчанию), чтобы оставить возможность отложенной настройки (Листинг 41).

Листинг 41. Инициатор с дополнительными конструкторами

template<typename CallbackArgument>

class Initiator

{

public:

  Initiator() {}

  Initiator(const CallbackArgument& argument) : callbackHandler(argument) {}

  void setup(const CallbackArgument& argument)

  {

    callbackHandler = argument;

  }

  void run()

  {

    int eventID = 0;

    //Some actions

    callbackHandler(eventID);

  }

private:

  CallbackArgument callbackHandler;

};

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

А какой тип аргумента нам указывать при инстанциировании шаблона, ведь тип лямбда-выражения является анонимным? Для этой цели мы будем использовать ключевое слово decltype, которое возвращает тип объявленной переменной (см. Листинг 42).

Листинг 42.Инстанциирование шаблона асинхронного обратного вызова для лямбда-выражения

int capture = 10;

auto lambda = [capture](int eventID) {/*this is a body of lambda*/};

Initiator<decltype(lambda)> callbackLambda1 (lambda); // Ok, initialization in constructor

Initiator<decltype(lambda)> callbackLambda = lambda; // Ok, implicit constructor call

Initiator<decltype(lambda)> callbackLambda2;  //Error: attempting to reference a deleted function

callbackLambda.setup(lambda);  //Error:  ‘operator’ =  attempting to reference a deleted function

callbackLambda.run();

4.4.3. Исполнитель

В Листинг 43 приведены примеры реализации исполнителя для различных типов аргументов. Объявления класса CallbackConverter представлены в Листинг 27 и Листинг 28 п. 4.2.2, инициатор используется из Листинг 41 п. 4.4.2.

Листинг 43. Исполнитель для шаблона-инициатора с различными типами аргумента

class Executor  // (1)

{

public:

  static void staticCallbackHandler(int eventID, Executor* executor) {}

  void callbackHandler(int eventID) {}

  void operator() (int eventID) {}

};

void ExternalHandler(int eventID, void* somePointer) {}  // (2)

int main()

{

  Executor executor;  // (3)

  int capturedValue = 0;

  // (4)  Pointer to the external function

  using PtrExtFunc = void(*) (int, void*);                                 // (5)

  using CallbackExtFunction = CallbackConverter<PtrExtFunc, void*>;        // (6)

  Initiator<CallbackExtFunction> initExtFunction;                          // (7)

  initExtFunction.setup(CallbackExtFunction(ExternalHandler, &executor));  // (8)

  // (9) Pointer to the static method

  using PtrStaticMethod = void(*) (int, Executor*);  // (10)

  using CallbacStaticMethod = CallbackConverter<PtrStaticMethod, Executor*>;                // (11)

  Initiator<CallbacStaticMethod> initStaticMethod;                                          // (12)

  initStaticMethod.setup(CallbacStaticMethod(Executor::staticCallbackHandler, &executor));  // (13)

  // (14) Pointer to the class member method

  using PtrMethod = void(Executor::*)(int);                                             // (15)

  using CallbackMemberMethod = CallbackConverter<Executor, void(Executor::*)(int)>;     // (16)

  Initiator<CallbackMemberMethod> initMemberMethod;  // (17)

  initMemberMethod.setup(CallbackMemberMethod(&executor, &Executor::callbackHandler));  // (18)

  // (19) Functional object

  Initiator<Executor> initFunctionObject;  // (20)

  initFunctionObject.setup(executor);      // (21)

  // (22) Lambda-expression

  auto lambda = [capturedValue](int eventID) {/*Body of lambda*/};  // (23)

  Initiator<decltype(lambda)> initLambda ( lambda);                 // (24)

}

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

В строке 4 показан обратный вызов через указатель на функцию. Объявлен тип указателя на функцию 5, тип функционального объекта для преобразования вызова 6, инстанциирование шаблона инициатора соответствующим типом 7, настройка инициатора 8. Запуск инициатора (метод run) не показан, чтобы не загромождать описание.

В строке 9 показан обратный вызов через указатель на статический метод класса. Похоже на предыдущий случай, только в качестве контекста используется указатель на класс. Объявлен тип указателя на статический метод 10, тип функционального объекта для преобразования вызова 11, инстанциирование инициатора соответствующего типа 12, настройка инициатора 13.

В строке 14 показан обратный вызов через указатель на метод-член класса. Объявлен тип указателя на метод 15, тип функционального объекта для преобразования вызова 16, инстанциирование инициатора соответствующим типом 17, настройка инициатора 18.

В строке 19 показан обратный вызов с помощью функционального объекта. Инстанциирование инициатора объявлено в строке 20, настройка инициатора – в строке 21.

В строке 22 показан обратный вызов с помощью лямбда-выражения. В строке 23 объявлено лямбда-выражение, которое запоминается в соответствующей переменной. В строке 24 инстанциирован инициатор типом лямбда-выражения. Инициатору в конструкторе передается переменная – объект указанного выражения.

Для случаев, когда используется преобразование вызовов (объявления 4, 9 и 14), можно использовать сокращенные объявления без использования промежуточных деклараций. Код в этом случае получается более компактным, но менее понятным (см. Листинг 44).

Листинг 44. Компактный способ объявлений при использовании преобразования вызовов

int main

{

  Executor executor;

  // (4) Pointer to the external function

  Initiator<CallbackConverter<void(*)(int, void*), void*>> initExtFunction;

  initExtFunction.setup(CallbackConverter<void(*)(int, void*), void*>(ExternalHandler, &executor));

  // (9) Pointer to the static method

  Initiator<CallbackConverter<void(*)(int, Executor*), Executor*>> initStaticMethod;

  initStaticMethod.setup(CallbackConverter<void(*)(int, Executor*), Executor*>(Executor::staticCallbackHandler, &executor));

  // (14) Pointer to the class member method

  Initiator<CallbackConverter<Executor, void(Executor::*)(int)>> initMemberMethod;

  initMemberMethod.setup(CallbackConverter<Executor, void(Executor::*)(int)>(&executor, &Executor::callbackHandler));

}

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

19Мы употребили термин «частично», потому что полной независимости здесь нет: при изменении функционального объекта нужно перекомпилировать как инициатор, так и исполнитель. Таким образом, независимость здесь обеспечивается только на уровне исходного кода.
20Указанная проблема решается при использовании универсального аргумента, о чем пойдет речь в следующей главе