Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Краткий обзор

Статические интерфейсы полезны как ограничения над параметрами шаблонов. В дополнение к обычному 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. Однако нет механизма для указания ограничений по параметрам шаблонов, которые могут быть привязаны к различным типам на этапе компиляции. В результате шаблонный код должен оставлять ограничения неявными или более разумно использовать абстрактную иерархию классов (из-за чего страдает производительность).

Эмуляция статических интерфейсов в C++

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.