Статические интерфейсы полезны как ограничения над параметрами шаблонов.
В дополнение к обычному template <class T>
, дефинция шаблона может
указывать, что T является (isa) подклассом некоторого статического интерфейса
Foo. Отношение isa-ограничений может быть основано на:
- наследовании (именованное соответствие:
T
публично наследуетFoo
); - членах (структурное соответствие: у
T
есть функции-члены с заданными сигнатурами); - оба вариант.
Механизм ограничений не требует пространства или временных оверхедов в runtime.
Две ключевые полезности статических интерфейсов:
Ограничения улучшают сообщения об ошибках в шаблонном коде. Применяя статические шаблоны как ограничения, инстанцирование шаблона с ошибочным типом может быть поймано в точке инстанцирования. Авторы шаблонных классов и шаблонных функций могут диспатчить пользовательские сообщения об ошибках, чтобы сообщить о нарушениях ограничений.
Ограничения позволяю автоматически на этапе компиляции диспетчеризировать
различные имплементации класса или функции основываясь на именном соответствии
свойств шаблонных типов. Например, Set<T>
может быть так написано, чтобы
выбирать наиболее эффективную имплементаци:
- hashtable implementation если “T isa Hashable”;
- binary search tree если “T isa LessThanComparable”;
- linked-list если “T isa EqualityComparable”.
Эта диспетчеризация может быть полностью спрятана от клиентов Set
, которые
используют просто Set<T>
.
Традиционные "динамические интерфейсы" на абстрактных классах:
struct Printable {
virtual ostream& print_on( ostream& o ) const =0;
// Функция позволяет работать коду “std::cout << p” для любого Printable
// объекта p.
};
struct PFoo : public Printable { // “PFoo isa Printable”
ostream& print_on( ostream& o ) const {
o << "I am a PFoo" << endl;
return o;
}
};
Здесь есть динамический полиморфизм. Переменная Printable
может быть
привязана в runtime к любому конкретному объекту Printable
.
Именованное и структурное соответствие обеспечиваются компилятором:
наследование является явным механизмом для декларация намерения быть
подклассом (именованное соответствие), а чистая виртуальная функция
подразумевает, что конкретные подклассы должны определить операцию с данной
сигнатурой (структурное соответствие).
Проблема такого механизма выражения интерфйсов в том, что иногда он избыточен. Виртуальные функции являются хорошим способом выражения интерфейсов, когда нам нужен динамический полиморфизм. Но иногда нам нужен только статический полиморфизм. В таких случаях интерфейсы на абстрактных классах неэффективны. Они добавляют vtable оверхеду каждого экземпляра конкретных объектов (пространство). Они добавляют точку косвенности (окольный путь) в вызовы методов в интерфейсах (runtime), virtual вызовы вряд ли будут за-inline-ны (оптимизация).
Библиотеки, которые используют шаблоны для статического полиморфизма избегают virtual. Например, STL. Но в языке нет явных конструкций для выражения статических интерфейсов. Единственный способ сказать, что T “isa” Printable и этот тип поддерживает метод print_on() с особенной сигнатурой функции это использовать абстрактные классы.
Короче говоря в C++ есть механизм для динамического полиморфизма, но нет аналогичного механизма для статического полиморфизма. Абстрактные классы позволяют пользователям указывать ограничения по параметрам для функций, которые могут быть привязаны к различным объектам в run-time. Однако нет механизма для указания ограничений по параметрам шаблонов, которые могут быть привязаны к различным типам на этапе компиляции. В результате шаблонный код должен оставлять ограничения неявными или более разумно использовать абстрактную иерархию классов (из-за чего страдает производительность).
template <class T> struct LessThanComparable {
MAKE_TRAITS; // макрос для определения ассоциированного класса
template<class Self> static void check_structural() {
bool (Self::* x)(const T&) const = &Self::operator<;
(void) x; // подавляет предупреждение "unused variable"
}
protected:
~LessThanComparable() {}
};
Здесь закодирована проверка структурного соответствия как шаблонная функция-член которая будет явно определеная где-нибудь в другом месте с привязкой Self к конкретному типу, чтобы убедиться, что типы соответствуют структурно. Эта функция берет указатели на желаемые member-ы чтобы убедиться, что они существуют. Защищенный деструктор предотвращет от того, чтобы кто-нибудь создавал напрямую экземпляры этого класса (но позволяет подкласса инстанцироваться).
Для указания того, что тип соответствует частному статическому интерфейсу используется:
struct Foo : public LessThanComparable<Foo> {
bool operator<( const Foo& ) const { ... }
// whatever other stuff
};
Конструкция StaticIsA определяет соответствует ли тип статическому интерфейсу.
StaticIsA< T, SI >::valid
Это логическое значение вычисляется на этапе компиляции и говорит о том,
соответствует ли тип T статическому интерфейсу SI. Другими словами значение
истино, если “T isa SI”. Например в Sort()
мы можем использовать
StaticIsA< T, LessThanComparable<T> >::valid
для определения, что частная инстанция шаблонной функциии в порядке.
Логическое значение StaticIsA<T,SI>::valid
вычисляется на этапе
компиляции. Мы можем использовать специализацию шаблонов для выбора
различных альтернатив для шаблона на этапе компиляции основываясь на
соответствии T
.