четверг, 9 июля 2009 г.

Новое в Си++: концепты

Этот текст содержит краткий обзор основных возможностей концептов – нововведения в новый стандарт Си++, позволяющего накладывать ограничения на параметры шаблонов. Показаны преимущества концептов при диагностике ошибок в шаблонах, использование отображений концептов для адаптации конкретных типов к использованию в качестве параметров шаблонов.

Обзор по моей просьбе написан Станиславом Михалковичем, доцентом Южного Федерального университета (Ростов-на-Дону), и выкладывается здесь с его любезного разрешения. Я сделал только очень небольшие редакторские правки и добавил немного замечаний (синим шрифтом), иногда эмоционально-окрашенных. J

В обзор вошли не все аспекты механизма концептов; в частности, ничего не сказано об архетипах и аксиомах. Но не все же сразу. J От обсуждений типа «зачем это нужно», «нужно ли вообще» давайте пока воздержимся. В том или ином виде ограничения на параметры шаблона можно задавать почти во всех современных языках, содержащих подобные средства: в Аде, в Java/C#, в Scala.

Итак, концепты в новом C++ – это средства задания ограничений на параметры шаблонов. Они представляют собой специальным образом оформленный набор объявлений, которые параметр шаблона должен реализовывать.

Рассмотрим вначале пример шаблона стандартного алгоритма find без использования концептов:

template<typename Iter, typename V>
Iter find(Iter first, Iter last, V v) {
while (first != last && *first != v)
++first;
return first;
}

Очевидно, что тип Iter должен удовлетворять следующим условиям:

  1. Объекты типа Iter должны быть сравнимы на !=.
  2. К объектам типа Iter можно применять префиксную операцию ++.
  3. Объекты типа Iter можно разыменовывать.

Кроме того, имеется условие, связывающее типы Iter и V:

  1. Разыменованные объекты типа Iter можно сравнивать с объектами типа V на !=.

В C++ стандарта 1998/2003 гг. проверка данных условий осуществляется при настройке шаблона. Например, при компиляции вызова find(1,5,0) в результате выведения получим: Iter=int, V=int. При попытке компиляции тела функции find с указанными типами возникнет ошибка: условие 3 не выполняется. Таким образом, ошибка компиляции возникнет на достаточно позднем этапе – при попытке компиляции тела настроенной версии find<int,int>. На практике это приводит к неадекватным по существу и совершенно безумным по форме диагностическим сообщениям, в которых фигурируют длиннейшие абсолютно нечитаемые имена настроенных шаблонов (в которых уже сделаны подстановки).

Рассмотрим теперь версию того же алгоритма find с использованием концептов:

template<typename V>
requires EqualityComparable
Iter find(Iter first, Iter last, V v) { ... }

Здесь тип Iter удовлетворяет концепту InputIterator, содержащему требования 1, 2 и 3, а также типы Iter::value_type и V связаны концептом EqualityComparable, содержащим требование 4. Теперь при вызове find(1,5,0) компилятор осуществит проверку типов Iter=int и V=int на соответствие концептам InputIterator и EqualityComparable, и мы получим сообщение об ошибке вида «Тип int не удовлетворяет концепту InputIterator». При этом в отличие от предыдущего примера тело функции find не будет компилироваться вообще, т.е. ошибка компиляции обнаружится на более раннем этапе.

Заметим, что условие удовлетворения типов концепту может записываться как в секции requires, так и в угловых скобках; в последнем случае ключевое слово typename заменяется на имя концепта, которому должен удовлетворять тип. Задание имени концепта в угловых скобках – более простой способ, он позволяет наложить на тип ограничения одного концепта. Для типов, удовлетворяющих нескольким концептам, а также для концептов, затрагивающих несколько типов, следует использовать вариант с requires, разделяя различные требования символом &&. Так, последний пример можно записать следующим образом:

template<typename Iter, typename V>
requires InputIterator && EqualityComparable
Iter find(Iter first, Iter last, V v) { ... }

Концепты позволяют обеспечить раннюю проверку типов не только при настройке шаблона, но и при его описании. Предположим, мы совершили ошибку в теле шаблона, неверно используя операцию <:

template<typename V>
requires EqualityComparable
Iter find(Iter first, Iter last, V v) {
while (first < last && *first != v)
++first;
return first;
}

Такое описание find не позволяет использовать его для списков: в итераторе списка отсутствует операция <. Без использования концептов ошибка будет обнаружена только при попытке компиляции тела настроенного шаблона find<list<int>::iterator,int>, т.е. на позднем этапе. При использовании концептов ошибка компиляции будет получена при первой компиляции тела шаблона и проверке входящих в него типов на соответствие концептам InputIterator и EqualityComparable, т.е. на существенно более раннем этапе.

Приведем определение концепта EqualityComparable:

template<typename T>
concept EqualityComparable {
bool operator==(T, T);
bool operator!=(T, T);
};

Оно напоминает определение интерфейса в таких языках программирования как Java и C#. Однако, в Java и C# уже при определении типа T надо описать все возможные интерфейсы, которым он удовлетворяет. Концепты позволяют, не затрагивая определение типа T, наложить на него ограничения позже. Такой способ является значительно более гибким, поскольку невозможно заранее предугадать все требования, накладываемые на тип T при его определении.

Функции, определенные внутри концепта, называются ассоциированными функциями. Так, в предыдущем объявлении bool operator==(T, T) является ассоциированной функцией. Кроме этого, внутри концепта могут определяться так называемые ассоциированные типы. Ассоциированные типы, как правило, используются для представления типов параметров и типов возвращаемых значений ассоциированных функций. Например, операция operator*, определенная в концепте InputIterator, возвращает некий тип value_type, который следует определить в концепте InputIterator следующим образом:

concept InputIterator<typename Iter> {
typename value_type;
value_type operator*(Iter);
...
};

Все типы, удовлетворяющие концепту InputIterator, должны определять тип InputIterator::value_type, а ссылка на этот тип может фигурировать, например, при наложении ограничений в секции requires, как в примере с алгоритмом find:

requires EqualityComparable

Обычно концепты определяются со служебным словом auto (такие концепты называются автоматическими):

auto template<typename T>
concept EqualityComparable {
bool operator==(T, T);
bool operator!=(T, T);
};

В этом случае любой тип, имеющий операции == и !=, удовлетворяет концепту. В частности, таковыми являются все стандартные числовые типы, тип string и пр. Если концепт определен без служебного слова auto или если в типе отсутствуют функции или операции с такими именами, то для того, чтобы тип удовлетворял концепту, необходимо объявить для него так называемое отображение концепта (concept_map). Отображение концепта для данного типа должно удовлетворять каждой ассоциированной функции и реализовывать каждый ассоциированный тип из концепта. Например, чтобы тип

struct Person {
string name;
int age;
};

удовлетворял концепту EqualityComparable в смысле равенства только полей name, необходимо объявить следующее отображение концепта:

concept_map EqualityComparable {
bool operator==(const Person& p1, const Person& p2)
{ return p1.name == p2.name; }
bool operator!=(const Person& p1, const Person& p2);
{ return !(p1 == p2); }
};

Чтобы не повторять очевидную реализацию operator!=, в концепте EqualityComparable следует задать для operator!= реализацию по умолчанию:

auto template<typename T>
concept EqualityComparable {
bool operator==(T, T);
bool operator!=(T t1, T t2) { return !(t1 == t2); }
};

В этом случае если объявление операции != в типе или его отображении концепта отсутствует, то берется реализация по умолчанию.

Отображение концепта само может быть шаблонным. Например, чтобы тип vector<T> удовлетворял концепту

concept Stack<typename X> {
typename value_type;
void push(X&, value_type);
void pop(X&);
};

следует определить шаблон отображения концепта

template<typename T>
concept_map Stack > {
typedef T value_type;
void push(std::vector& v, const T& x) { v.push_back(x); }
void pop(std::vector& v) { v.pop_back(); }
};

Обратим внимание, что отображение ассоциированного типа выполняется с помощью директивы typedef.

Концепты могут наследоваться (уточняться). Например:

concept ForwardIterator<typename Iter> { ... };
concept RandomAccessIterator<typename Iter>: ForwardIterator
{ ... };

При этом при наличии нескольких совместно используемых версий функции, на типы которой наложены ограничения концептов, выбирается наиболее специализированная версия концепта. Так, при наличии следующих совместно используемых объявлений

template
void advance(Iter& p, int n) { while (n--) ++p; }
template
void advance(Iter& p, int n) { p += n; }

при вызове

vector<int> v(10);
vector<
int>::iterator vi = v.begin();
advance(vi,5);

предпочтение отдается более специализированной версии advance, в которой тип Iter удовлетворяет концепту RandomAccessIterator.

В качестве завершения нужно сказать, что концепты – весьма мощное средство, которое по своим возможностям и гибкости явно превосходит аналогичные механизмы в других языках. При том, что идея задания ограничений на параметры шаблонов представляется совершенно ясной, конкретная ее реализация в «новом» Си++, прямо скажем, выглядит весьма непростой, громоздкой и неочевидной. В этом смысле новый механизм идет в русле «философии» Си++: совершенно ясные, мощные и полезные идеи, лежащие в основе концептуального базиса языка, сопровождаются реализацией, которая как будто специально спроектирована так, чтобы программистам и разработчикам компиляторов «жизнь медом не казалась»...

10 комментариев:

Анонимный комментирует...

http://www.boostcon.com/site-media/var/sphene/sphwiki/attachment/2009/05/08/iterators-must-go.pdf

Анонимный комментирует...

Очепятка:
templatetypename V>

zouev комментирует...

Спасибо, поправил

Анонимный комментирует...

templatetypename осталось =/

Анонимный комментирует...

с концептами авторы как-то настолько перемудрили что никто не хочет их реализовывать :)
в итоге концепты тормозят принятие стандарта
Вы абсолютно правы - из всех путей в с++ всегда находят самый сложный :)

zouev комментирует...

Для Анонима:

с концептами авторы как-то настолько перемудрили что никто не хочет их реализовывать :)
в итоге концепты тормозят принятие стандарта


Ну, что значит "никто не хочет реализовывать"?- вот в gcc уже опция действует для новых возможностей. Куда денешься... :-)

А принятие стандарта концепты не тормозят - скорее, их добавление отодвинуло время передачи стандарта на обсуждение национальным комитетам...

Анонимный комментирует...

Концепты - всё :(

Комитет убрал их из стандарта.

Анонимный комментирует...

Да вообще дебилы. Что-то там нечленораздельное промычали про "unusable" концетов. Моё мнение такое - концепты обязаны быть. Даже в недошарпе есть where для параметров generic'ов. И если комитет не может родить нормальные концепты, гнать надо такой комитет. И не важно, сидит там создатель языка или лежит - в шею!!!

Tonal комментирует...

Что-то форматирование кода совсем плывёт. Или это blogger.com с FF 3.5 не очень совместим...

Действительно, очень жаль что с концептами не получилось.
Остаётся надеяться, что из gcc их не выкинут и к следующему раунду стандартизации фича будет заюзана во многих проектах и вылизана. :)

Анонимный комментирует...

Госопода, давайте концепции не будем обзывать концептами?