Типобезопасные идентификаторы и фантомные типы
Довольно часто в программе, работающей с базой данных, в качестве идентификаторов сущностей используются значения целочисленного типа (например, long). Но людям свойственно ошибаться, и программист может по ошибке использовать идентификатор одного типа сущности для адресации другой. Такая проблема может долго оставаться незамеченной, если идентификаторы сущностей пересекаются, а такое бывает довольно часто. К счастью, в языках, позволяющих манипулировать типами, коим является C++, есть довольно простое решение этой проблемы.Постановка проблемыПредположим, наша программа работает с несколькими типами сущностей. Для примера возьмём виджеты (класс Widget) и гаджеты (класс Gadget): class Widget { public: long id () const; // … };
class Gadget {
public:
long id () const;
// …
};
Помимо высокой вероятности ошибки, использование «сырых» типов в качестве идентификаторов существенно снижает читаемость кода. Не очень-то легко понимать код, содержащий множество типов вроде std: vector
template
IdOf () : value_() {}
explicit IdOf (repr_type value) : value_(value) {}
repr_type value () const { return value_; }
bool operator==(const IdOf &rhs) const { return value () == rhs.value (); }
bool operator!=(const IdOf &rhs) const { return value () != rhs.value (); }
bool operator<(const IdOf &rhs) const { return value() < rhs.value(); }
bool operator>(const IdOf &rhs) const { return value () > rhs.value (); }
private: repr_type value_; }; Применим новый класс к нашим гаджетам и виджетам: class Gadget; class Widget;
typedef IdOf
class Widget { public: WidgetId id () const; // … };
class Gadget {
public:
GadgetId id () const;
// …
};
Благодаря тому, как мы определили класс IdOf, следующий код, содержащий логические ошибки, не будет компилироваться:
// This won’t compile.
vector
// This won’t compile either. if (someGadget.id () == someWidget.id ()) { doSomething (); } Операции же над идентификаторами одного типа будут работать правильно. Теперь компилятор знает больше о наших намерениях, он не даст нам загрузить гаджет по идентификатору виджета или поместить в вектор идентификатор неправильного типа.Если нам всё же нужно будет сравнить идентификаторы разных типов, или сравнить идентификатор с «сырым» значением, всегда можно вызвать метод value () явно.
Фантомные типы Оказывается, трюк, который мы только что провернули с идентификаторами, известен в функциональном программировании довольно давно. Параметризованные типы, не использующие тип-параметр в определении, называются фантомными типами (Phantom Types).К примеру, в Haskell подобный приём может быть реализован следующим образом: newtype IdOf a = IdOf { idValue: Int } deriving (Ord, Eq, Show, Read) Ого, всего лишь пара строк кода! Теперь добавим определения наших моделей: data Widget = Widget { widgetId: IdOf Widget } deriving (Show, Eq) data Gadget = Gadget { gadgetId: IdOf Gadget } deriving (Show, Eq) и проверим желаемое поведение, создав экземпляры разных типов и попробовав сравнить их идентификаторы: Prelude> let g = Gadget (IdOf 5) Prelude> let w = Widget (IdOf 5) Prelude> widgetId w == gadgetId g
Итоги Всего один небольшой класс может сохранить нам немало времени, которое пришлось бы потратить на поиск ошибки. Кроме того, использование такого подхода не скажется на производительности и потреблении памяти программы во время выполнения при компиляции с включенной оптимизацией. Версия на Haskell также не вносит дополнительных накладных расходов.Недостатком является необходимость набирать (и читать) чуть больше букв и, возможно, объяснять идею коллегам, но довольно часто преимущества от более строгой проверки логики компилятором перевешивают недостатки.
Фантомные типы популярны в приложениях, требующих высокой надёжности, где каждая допольнительная проверка, автоматически совершаемая компилятором, сокращает убытки компании. В частности, они активно используются при программировании на OCaml в компании Jane Street и в продуктах банка Standard Chartered, написанных на Haskell (о чём рассказывал Don Stewart на Google Tech Talk 2015).
Нельзя также не упомянуть о мощной библиотеке Boost.Units, позволяющую производить типобезопасные операции над значениями разных типов с автоматическим выводом типа результата.