Выбрать главу
Листинг 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));

}

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

4.5. Универсальный аргумент

4.5.1. Динамический полиморфизм

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

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

Графическое изображение стирания типов изображено на Рис. 17. Рассмотрим начальное состояние а), показанное в верхней части рисунка. Имеется некоторый класс, назовем его UniArgument. В этом классе объявлен перегруженный оператор вызова функции 2. Также здесь имеется указатель 3 типа Callable*, который указывает на соответствующий экземпляр класса Callable. Класс Callable 4 объявлен внутри UniArgument и имеет виртуальный перегруженный оператор вызова функции с пустой реализацией.

Когда в UniArgument происходит вызов 1 перегруженного оператора 2, последний через указатель 3 вызывает виртуальный перегруженный оператор класса Callable.

В нижней части рисунка б) показано, как назначается новый тип. Объявляется перегруженный оператор присваивания 10, на входе он принимает аргумент обратного вызова 8. При вызове этого оператора старый экземпляр класса 4, на который указывал указатель 3, уничтожается в 11, а вместо него создается новый класс CallableObject 5, который наследуется от Callable. Внутри класса имеется поле 7, в которое записывается переданный аргумент 8, тип этого поля совпадает с типом аргумента. В CallableObject переопределяется оператор вызова функции 6, который, в свою очередь, осуществляет вызов через сохраненный аргумент 7. Теперь указатель 3 указывает на новый созданный CallableObject, и при вызове 1 перегруженного оператора 2 будет вызываться перегруженный оператор указанного класса, который и выполнит обратный вызов.

Рис. 17. Стирание типов: а) исходное состояние; б) состояние после назначения нового типа аргумента

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

Листинг 45. Класс, реализующий стирание типов

class UniArgument  // (1)

{

private:

  class Callable   // (2)

  {

  public:

    virtual void operator()(int) = 0;         // (3)

  };

  std::unique_ptr<Callable> callablePointer;  // (4)

  template <typename ArgType>

  class CallableObject : public Callable      // (5)

  {

  public:

    CallableObject(ArgType argument) : storedArgument(argument) { }  // (6)

    void operator() (int value) override  // (7)

    {

      storedArgument(value);  // (8)

вернуться

21

Термин «динамический полиморфизм» означает, что полиморфизм реализуется во время выполнения программы. В противоположность этому, статический полиморфизм реализуется на этапе компиляции программы. В строгом смысле этого термина динамический полиморфизм в C++ нереализуем, поскольку это язык со статической типизацией. Однако его можно смоделировать с помощью наследования и шаблонов, о чем пойдет речь далее.

вернуться

22

Для фундаментального изучения техники стирания типов можно порекомендовать книгу «Пикус Ф.Г.

Идиомы и паттерны проектирования в современном С++», в которой указанной технике посвящена отдельная глава.