Реализация паттерна «Стратегия» посредством класса tr::function

Реализация паттерна «Стратегия» посредством класса tr::function

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

Эти ограничения исчезают, если вместо указателя на функцию (подобную healthFunc) воспользоваться объектом типа tr::function. Как объясняется в правиле 54, такой объект может содержать любую вызываемую сущность (указатель на функцию, функциональный объект либо указатель на функцию-член), чья сигнатура совместима с ожидаемой. Вот пример такого подхода, на этот раз с использованием tr1::function:

class GameCharacter; // как раньше

int defaultHealthCalc(const GameCharacter& gc); // как раньше

class GameCharacter {

public:

// HealthCalcFunction – это любая вызываемая сущность, которой можно

// передать в качестве параметра нечто, совместимое с GameCharacter,

// и которая возвращает нечто, совместимое с int; подробности см. ниже

typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;

explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

: healthFunc(hcf)

{}

int healthValue() const

{ return healthFunc(*this);}

...

private:

HealthCalcFunc healthFunc;

};

Как видите, HealthCalcFunc – это typedef, описывающий конкретизацию шаблона tr1::function. А значит, он работает как обобщенный указатель на функцию. Посмотрим внимательнее, как определен тип HealthCalcFunc:

std::tr1::function<int (const GameCharacter&)>

Здесь я выделил «целевую сигнатуру» данной конкретизации tr1::function. Словами ее можно описать так: «функция, принимающая ссылку на объект типа const GameCharacter и возвращающая int». Объект типа HealthCalcFunc может содержать любую вызываемую сущность, чья сигнатура совместима с заданной. Быть совместимой в данном случае означает, что параметр можно неявно преобразовать в const GameCharacter&, а тип возвращаемого значения неявно конвертируется в int.

Если сравнить с предыдущим вариантом дизайна (где GameCharacter включал в себя указатель на функцию), то вы не обнаружите почти никаких отличий. Единственная разница в том, что GameCharacter теперь содержит объект типа tr1::function – обобщенный указатель на функцию. Это изменение так незначительно, что я назвал бы его несущественным, если бы не то обстоятельство, что теперь пользователь получает ошеломляющую гибкость в спецификации функций, вычисляющих жизненную силу:

short calcHealth(const gameCharacter&); // функция вычисления

// жизненной силы;

// она возвращает не int

stuct HealthCalculator { // класс функциональных

int operator()(const GameCharacter&) const // объектов, вычисляющих

{...} // жизненную силу

};

class GameLevel {

public:

float health(const GameCharacter&) const; // функция-член для

... // вычисления жизненной

}; // силы; возвращает не int

class EvilBadGay: public GameCharacter { // как раньше

...

};

class EyeCandyCharacter: public GameCharacter { // другой тип персонажей;

... // предполагается такой же

}; // конструктор как

// у EvilBadGay

EvilBadGay ebg1(calcHealh); // персонаж использует

// функцию вычисления

// жизненной силы

EyeCandyCharacter ecc1(HealthCalculator()); // персонаж использует

// функциональный объект

// вычисления жизненной

// силы

GameLevel currentLevel;

...

EvilBadGay ebg2( // персонаж использует

std::tr1::bind(&GameLevel::health, // функцию-член для

currentLevel, // вычисления жизненной

_1) // силы; подробности

); // см. ниже

Лично я поражаюсь тому, какие удивительные вещи позволяет делать шаблон tr1::function. Если вы не разделяете моих чувств, то не исключено, что просто не понимаете, для чего используется tr1::bind в определении ebg2. Позвольте мне объяснить.

Мы хотим сказать, что для вычисления жизненной силы персонажа ebg2 следует использовать функцию-член класса GameLevel. Но из объявления GameLevel::health следует, что она должна принимать один параметр (ссылку на GameCharacter), а на самом деле она принимает два, потому что имеется еще неявный параметр типа GameLevel – тот, на который внутри нее указывает this. Все функции вычисления жизненной силы принимают лишь один параметр: ссылку на персонажа GameCharacter, чья жизненная сила вычисляется. Если мы используем функцию GameLevel::health, то должны каким-то образом «адаптировать» ее, чтобы вместо двух параметров (GameCharacter и GameLevel) она принимала только один (GameCharacter). В этом примере мы хотим для вычисления здоровья ebg2 в качестве параметра типа GameLevel всегда использовать объект currentLevel, поэтому «привязываем» его как первый параметр при вызове GameLevel::health. Именно в этом и заключается смысл вызова tr1::bind: указать, что функция вычисления жизненной силы персонажа ebg2 должна в качестве объекта типа GameLevel использовать currentLevel.

Я пропускаю целый ряд подробностей, к примеру: почему «_1» означает «использовать currentLevel в качестве объекта GameLevel при вызове GameLevel::health для ebg2». Эти детали не столь сложны, к тому же они не имеют прямого отношения к основной идее, которую я хочу продемонстрировать, а именно: используя tr1::function вместо указателя на функцию, мы позволяем пользователям применять любую совместимую вызываемую сущность для вычислении жизненной силы персонажа. Впечатляет, не правда ли?

Данный текст является ознакомительным фрагментом.