Pull to refresh

Концепции Boost

Reading time 6 min
Views 32K
От использования шаблонов в С++ лично меня всегда отпугивало отсутствие стандартных механизмов задания ограничений параметров. Другими словами, когда разработчик пишет функцию

template <class T>
bool someFunc(T t)
{
	if (t.someCheck()) {
		t.someAction(0);
	}
}

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

  1. Объекты типа T передаются по значению, значит, должны иметь открытый копирующий конструктор
  2. Существует открытый метод T::someCheck без параметров, который возвращает значение, приводимое к логическому типу
  3. Существует отрытый метод T::someAction, который может принимать один приводимый к числовому типу параметр

Проблема

Теперь, допустим, программист решил распространять someFunc в виде библиотеки. Как ее пользователь может узнать о существующих ограничениях?
  1. Чтение документации к библиотеке. Если она есть и внятно написана. Но даже в этом случае никто не будет вычитывать документацию всех используемых библиотек перед каждым изменением своего кода. Помнить все условия наизусть тоже не каждому по плечу.
  2. Изучение исходного кода библиотеки. Тоже занятие на любителя. Причем чем библиотека больше и свой проект сложнее, тем любителей меньше

Остается еще один вариант — по сути, единственный автоматический — отталкиваться от ошибок компиляции. Т.е. сделал изменение, не собирается, ищешь почему… Однако те, кто пользовался шаблонами C++ знают, на что могут быть похожи сообщения об ошибках. На что угодно, только не на подсказку вида «Исправь вот здесь, и все заработает». Иногда сообщение достаточно понятно, а иногда оказываешься в дебрях чужой библиотеки… Компилятор сообщает об ошибке в том месте, где она произошла — ему все равно, что первоначальный контекст использования там уже не восстановить.

Рассмотрим пример (мы еще вернемся к нему позже)

Нужно отсортировать список (стандартный контейнер). Ничего не предвещает, пишем

std::list<int>theList;
std::sort(theList.begin(), theList.end());

Не компилируется. В VS2013 ошибка выглядит следующим образом
error C2784: 'unknown-type std::operator -(std::move_iterator&lt_RanIt&gt &,const std::move_iterator&lt_RanIt2> &)': could not deduce template argument for 'std::move_iterator&lt_RanIt> &' from 'std::_List_iterator&ltstd::_List_val&ltstd::_List_simple_types<int&gt&gt&gt' c:\program files (x86)\microsoft visual studio 12.0\vc\include\algorithm 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' boost\concept_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 boost\concept_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 boost\boost\concept_check.hpp

Щелчок по ошибке — и мы оказываемся в исходнике LessThanComparable, понимая, что именно нарушили.

Таким образом, концепции делают обобщенное программирование в C++ чуть менее экстремальным. Что не может не радовать!
Tags:
Hubs:
+74
Comments 30
Comments Comments 30

Articles