Skip to content

Latest commit

 

History

History

static-interfaces

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

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

Статические интерфейсы полезны как ограничения над параметрами шаблонов. В дополнение к обычному 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.