От использования шаблонов в С++ лично меня всегда отпугивало отсутствие стандартных механизмов задания ограничений параметров. Другими словами, когда разработчик пишет функцию
template <class T>
bool someFunc(T t)
{
if (t.someCheck()) {
t.someAction(0);
}
}
он делает различные допущения относительно функциональности объектов типа T, однако не имеет стандартной возможности донести их до пользователей. Так приведенный пример предполагает, как минимум, следующее
- Объекты типа T передаются по значению, значит, должны иметь открытый копирующий конструктор
- Существует открытый метод T::someCheck без параметров, который возвращает значение, приводимое к логическому типу
- Существует отрытый метод T::someAction, который может принимать один приводимый к числовому типу параметр
Проблема
Теперь, допустим, программист решил распространять someFunc в виде библиотеки. Как ее пользователь может узнать о существующих ограничениях?
- Чтение документации к библиотеке. Если она есть и внятно написана. Но даже в этом случае никто не будет вычитывать документацию всех используемых библиотек перед каждым изменением своего кода. Помнить все условия наизусть тоже не каждому по плечу.
- Изучение исходного кода библиотеки. Тоже занятие на любителя. Причем чем библиотека больше и свой проект сложнее, тем любителей меньше
Остается еще один вариант — по сути, единственный автоматический — отталкиваться от ошибок компиляции. Т.е. сделал изменение, не собирается, ищешь почему… Однако те, кто пользовался шаблонами C++ знают, на что могут быть похожи сообщения об ошибках. На что угодно, только не на подсказку вида «Исправь вот здесь, и все заработает». Иногда сообщение достаточно понятно, а иногда оказываешься в дебрях чужой библиотеки… Компилятор сообщает об ошибке в том месте, где она произошла — ему все равно, что первоначальный контекст использования там уже не восстановить.
Рассмотрим пример (мы еще вернемся к нему позже)
Нужно отсортировать список (стандартный контейнер). Ничего не предвещает, пишем
std::list<int>theList;
std::sort(theList.begin(), theList.end());
Не компилируется. В VS2013 ошибка выглядит следующим образом
error C2784: 'unknown-type std::operator -(std::move_iterator<_RanIt> &,const std::move_iterator<_RanIt2> &)': could not deduce template argument for 'std::move_iterator<_RanIt> &' from 'std::_List_iterator<std::_List_val<std::_List_simple_types<int>>>' c:program files (x86)microsoft visual studio 12.0vcincludealgorithm 3157 1 MyApp
Но это полбеды — при клике по ошибке мы оказываемся в глубинах стандартной библиотеки algorithm вот в этом месте
template<class _RanIt,
class _Pr> inline
void sort(_RanIt _First, _RanIt _Last, _Pr _Pred)
{ // order [_First, _Last), using _Pred
_DEBUG_RANGE(_First, _Last);
_DEBUG_POINTER(_Pred);
_Sort(_Unchecked(_First), _Unchecked(_Last), _Last - _First, _Pred);
}
Первая реакция: «Чего?! Почему вектор сортировался, а список вдруг нет — у обоих контейнеров есть итераторы, оба знают о порядке элементов..» И ладно еще стандартная библиотека — этот пример избит, и программисты обычно знают, что случилось. Но представьте, что вас вот так без спасательного круга бросили в недра другой, не такой известной библиотеки…
Решение
Оказывается, решение есть. Инициатива изменения языка в этом направлении существует, но пока в стандарт не попала.
А вот библиотека boost поддерживает понятие концепций (concepts), с помощью которых можно создавать пользовательские ограничения для параметров шаблонов.
Алгоритм использования концепций следующий. Разработчик вместе со своими библиотеками поставляет описание необходимых для их корректной работы концепций. Пользователь может в автоматическом режиме тестировать все свои сущности на соответствие предложенным правилам. При этом ошибки уже будут гораздо понятнее, вида: Класс не поддерживает концепцию «Должен быть конструктор по умолчанию».
Используя boost, разработчик не обязан каждый раз конструировать концепции с нуля — библиотека содержит заготовки основных ограничений.
Рассмотрим пример для функции someFunc, приведенной в начале статьи. Первое правило — наличие копирующего конструктора покрывается готовой концепцией boost::CopyConstructible, для остальных придется написать тесты вручную.
#include <boost/concept_check.hpp>
template <class T>
struct SomeFuncAppropriate {
public:
BOOST_CONCEPT_ASSERT((boost::CopyConstructible<T>));
BOOST_CONCEPT_USAGE(SomeFuncAppropriate)
{
bool b = t.someCheck();// метод someCheck, с возвращаемым значением, приводимым к bool
t.someAction(0);// метод someAction с параметром, приводимым к числу
}
private:
T t; // must be data members
};
Итак, концепция boost — это структура-шаблон, в качестве параметра которого используется тестируемый тип. Проверка на соответствие готовым концепциям осуществляется посредством макроса BOOST_CONCEPT_ASSERT. Обратите внимание — в качестве параметра ему передается концепция в скобках, в итоге двойные скобки обязательны, хоть и режут глаз.
Пользовательские проверки могут быть реализованы с помощью макроса BOOST_CONCEPT_USAGE. Важно помнить, что все экземпляры, участвующие в тестировании (у нас это T t), должны быть объявлены как члены класса, а не как локальные переменные.
Когда концепция объявлена, на соответствие ей проверять можно с использованием того же макроса BOOST_CONCEPT_ASSERT. Допустим, у нас есть класс
class SomeClass
{
public:
SomeClass();
void someCheck();
int someAction(int);
private:
SomeClass(const SomeClass& other);
};
Протестировать его можно так
BOOST_CONCEPT_ASSERT((SomeFuncAppropriate<SomeClass>));
Пробуем запустить — сразу получаем ошибку
error C2440: 'initializing': cannot convert from 'void' to 'bool'
Причем при клике по ней, нас бросает на нарушенную строчку в определении концепции SomeFuncAppropriate (в BOOST_CONCEPT_USAGE), где можно легко понять причину проблемы — метод someCheck возвращает void вместо bool. Исправляет, пробуем еще раз…
error C2248: 'SomeClass::SomeClass': cannot access private member declared in class 'SomeClass' boostconcept_check.hpp
По клике на ошибке оказываемся в исходном коде концепции
BOOST_concept(CopyConstructible,(TT))
{
BOOST_CONCEPT_USAGE(CopyConstructible) {
TT a(b); // require copy constructor
TT* ptr = &a; // require address of operator
const_constraints(a);
ignore_unused_variable_warning(ptr);
}
...
Причем курсор указывает на строчку
TT a(b); // require copy constructor
Ах да — копирующий конструктор спрятан. Исправляем — теперь тест проходится (компилируется файл с BOOST_CONCEPT_ASSERT). Значит, класс SomeClass полностью соответствует ожиданиям разработчика функции someFunc. Даже если в будущем будут добавлены изменения, которые нарушат совместимость, проверка концепции сразу сообщит, в чем именно проблема.
Вернемся к примеру с сортировкой std::list с помощью std::sort. Выразим в виде концепции требования к сортируемому контейнеру. Во-первых, std::sort может работать только с контейнерами, которые поддерживают произвольный доступ (random access). Соответствующая концепция имеется в boost (boost::RandomAccessContainer), однако ее недостаточно. Также существует требование к содержимому контейнера — его элементы должны поддерживать оператор сравнения «меньше». Тут снова выручает boost с готовой концепцией boost::LessThanComparable.
Комбинируем концепции в одну
template <class T>
struct Sortable
{
public:
typedef typename std::iterator_traits<typename T::iterator>::value_type content_type;
BOOST_CONCEPT_ASSERT((boost::RandomAccessContainer<T>));
BOOST_CONCEPT_ASSERT((boost::LessThanComparable<content_type>));
};
Запускаем проверку
BOOST_CONCEPT_ASSERT((Sortable<std::list<int> >));
Видим
error C2676: binary '[': 'const std::list<int,std::allocator<_Ty>>' does not define this operator or a conversion to a type acceptable to the predefined operator boostconcept_check.hpp
Щелчок по ошибке отправляет нас в исходный код концепции RandomAccessContainer, давая понять, что именно она и нарушена. Если заменить std::list на std::vector, проверка концепции увенчается успехом. Теперь попробуем проверить на сортируемость вектор экземпляров SomeClass.
BOOST_CONCEPT_ASSERT((Sortable<std::vector<SomeClass> >));
Контейнер-то теперь подходящий, но отсортировать его все равно нельзя, так как SomeClass не определяет оператора «меньше». Об этом мы узнаем сразу
error C2676: binary '<': 'SomeClass' does not define this operator or a conversion to a type acceptable to the predefined operator boostboostconcept_check.hpp
Щелчок по ошибке — и мы оказываемся в исходнике LessThanComparable, понимая, что именно нарушили.
Таким образом, концепции делают обобщенное программирование в C++ чуть менее экстремальным. Что не может не радовать!
Автор: vadim_ig