Концепции Boost

  • Tutorial
От использования шаблонов в С++ лично меня всегда отпугивало отсутствие стандартных механизмов задания ограничений параметров. Другими словами, когда разработчик пишет функцию

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<_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.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++ чуть менее экстремальным. Что не может не радовать!
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 30

    +3
    Занятно. Посмотрел Copy Constructible — внутри по сути определение нового класса, а так как класс не используется, то будет выкинут на этапе линковки. Хорошая технология.
      +2
      Это мастхев фича. В языках новой волны требования к параметрам обобщений стараются делать сразу в основе языка.
      Например в Rust это сделано через трейты/типажи.
      В языке Ceylon более многословно, но и достаточно читабельно, через специальное ключевое слово. В последнем есть также поддержка ковариантности и контравариантности через ключевые слова out и in.
        +1
        Об этом(как-то «ограничения на типы» или что-то в этом роде), кстати, Страуструп писал в «Дизайн и эволюция C++». И это в 90тые годы — не помню, какие доводы были за и против.
          0
          Кому интересно — раздел 15.4 «Ограничения на аргументы шаблонов»
          0
          Полезная фича, вывод от ошибки в шаблоне может быть великоват.
          Насколько я понял, в стандарт не вошли только сами концепты. Вот, к примеру, is_copy_constructible помечен как «C++11». Пример по ссылке gcc 4.8.2 не собирается, поскольку
            /// is_trivially_constructible (still unimplemented)
          
          Но без строк содержащих is_trivially_constructible работает.
            0
            Да ладно, неужели VS не показывает откуда растут ноги у ошибки? gcc про любые ошибки пишет к какой строке пользовательского кода она относится:
            /usr/include/c++/4.8/bits/stl_algo.h: In instantiation of ‘void std::sort(_RAIter, _RAIter) [with _RAIter = std::_List_iterator<int>]’:
            test.cpp:7:45:   required from here
            /usr/include/c++/4.8/bits/stl_algo.h:5452:22: error: no match for ‘operator-’ (operand types are ‘std::_List_iterator<int>’ and ‘std::_List_iterator<int>’)
                 std::__lg(__last - __first) * 2);
            
              +1
              Хотя конечно такая ручная проверка — полезная штука.
              +1
              Сейчас все это неплохо реализуется с помощью static_assert и type-traits, имеющихся в стандартной библиотеке либо самописных. Boost, видимо, использует какие-то ухищрения, чтобы не требовать C++11?
                0
                Если я правильно понял, type-traits не про то. Тут не нужно проверять типы, тут нужно проверять, обладает ли класс какой-то функций. Хотя возможно type-traits и это умеет, но вроде нет.
                  +2
                  Я имею в виду вспомогательные шаблоны классов типа std::is_copy_constructible. В стандарте они называются type-traits, по сути так же отражают фундаментальные концепты.
                  en.cppreference.com/w/cpp/types/is_copy_constructible
                  Concepts отражают скорее не обладание набором функций, а обладание неким базовым поведением. Это может быть и набор каких-то функций (как для random_access_iterator), и просто поведение (например is_nothrow_move_constructible).
                0
                <not-a-cpp-developer>Не является ли ситуация, когда вам нужно сказать переменному типу, что он должен реализовать определенный интерфейс, той самой ситуацией когда как раз интерфейс и надо использовать?</not-a-cpp-developer>
                  +3
                  Бывает полиморфизм времени исполнения и времени компиляции. Программирование с использованием шаблонов это как раз второй вариант, и он дает широкие возможности для статической типизации и оптимизации результирующего кода.
                  Не всегда нужно и возможно применить наследование, чтобы выделить общий интерфейс. Можете считать это утиной типизацией.
                    0
                    Спасибо за ответ, тогда еще вопрос, эту проблему нельзя решить на уровне языка? Ведь можно было бы добавить сахар вида
                    template <class T implements RandomAccessibleIterator>
                    либо даже что-то вроде
                    actslike вместо implements чтобы избежать стат. связывания
                      +1
                      Ответил ниже, но не успел отредактировать съеденный тег. Выглядеть будет примерно как Вы себе представляете.
                      Связывание у шаблонов именно статическое, они позволяют использовать статическую типизацию на полную катушку.
                      Возможно Вы путаете generics в Java/C# и шаблоны C++. Первые, вроде, как раз и реализованы на динамическом связывании.
                        0
                        Под «избежать стат связывания» я имел ввиду конечного класса, который попадет в аргумент тайп-переменной, с интерфейсом :) То есть компатибилити-чек опять же произойдет в момент компиляции, когда компилятор наткнется на инстанциирование нового generic-класса (сори за использование термина «generic», но я не знаю как его надо было бы назвать на языке плюсов), он не будет смотреть имплементировал ли данный класс данный интерфейс, а будет просто проверять соответствует ли данный класс данному интерфейсу (можно назвать это неявной реализацией).

                        Еще раз спасибо за ответ, кстати соседняя статья про CRTP тоже проливает немного света на эти вопросы.
                  +2
                  Комитет планирует внести данные изменения в стандарт, условно называемый C++17. Ориентировочно использование выглядеть будет так:
                  template void sort (C& c);

                  Список фундаментальных концептов формируется и собирается уже сейчас и доступен например на cppreference.com, пока без поддержки со стороны языка.
                    0
                    Нанотех, безусловно, интересный. Однако, глядя на вспомогательный класс и на исходную ф-ию, для которой он был написан, заметно, что вспомогательного кода гораздо больше и он сложнее. Можно сослаться на надуманность примера. Но по-моему, чем сложнее будет испытуемый шаблонный код, тем сложнее и объемнее будет вспомогательный код для проверки концепций.
                      –2
                      Помню подобное в Visual Basic. Interface называется.

                        –3
                        Всегда поражался отсутствию конструкции interface в C++.
                          +1
                          Класс с чисто виртуальными функциями — это и есть интерфейс в С++. Но в этом случае нужная функция выбирается во время выполнения программы. В статье же речь про шаблоны, которые подставляются во время компиляции, что гораздо эффективнее. Вот для них в языке пока и нет подходящего способа для описания интерфейса (концепта).
                            –1
                            Ну так если бы в шаблонах можно было указывать интерфейс класса T, это решило бы проблему, правда?
                              +1
                              Нет, поскольку это привело бы нас к требованию классу T быть наследником некоторого интерфейса, а это другой тип полиморфизма, т.е. по сути то, ради ухода от чего шаблоны и придумывались. К примеру, мы хотим взять из одной библиотеки класс комплексного числа, а из другой — какой-то хитрый контейнер, в который мы эти числа будем складывать. Если класс комплексного числа удовлетворяет требованиям к элементу контейнера (конструктор копирования, оператор сравнения) — мы можем это сделать. Но если класс контейнера вдруг начнёт требовать чтобы его элементы обязательно наследовались от какого-то интерфейса IElement — мы уже не можем положить в этот контейнер наше комплексное число из другой библиотеки, поскольку оно, конечно, ничего об этом интерфейсе не знает.
                                0
                                А чем виртуальные классы лучше? То же самое, комплексное число из другой библиотеки может не наследовать этот виртуальный класс.
                                  0
                                  Ничем не лучше, поэтому и существуют концепции Boost, о которых и статья.
                                    0
                                    Эээ нет, извините.

                                    Допустим, у нас заявлен некоторый интерфейс.
                                    И, допустим, мы научили компилятор понимать, что некоторый класс соответствует этому интерфейсу даже если в сигнатуре класса *не* написано implements Interface.

                                    Всё, проблема решена. Не вижу принципиальных препятствий для этого.
                                    С абстрактными классами этот подход не проходит, если он содержит хоть один неабстрактный метод.
                                      0
                                      Допустим, у нас заявлен некоторый интерфейс.
                                      И, допустим, мы научили компилятор понимать, что некоторый класс соответствует этому интерфейсу даже если в сигнатуре класса *не* написано implements Interface.

                                      Всё, проблема решена.


                                      Да, решена. И это будет называться Concepts, активно пилится и, я надеюсь, скоро войдет в стандарт.

                              0
                              Не скажите. Интерфейс можно добавить к новому классу, даже если он унаследован уже от кого-то. И потом, их может быть несколько. Работает аналогично. Объвляешь переменную именем интерфейса, и не важно обьект какого класса в ней находится, главное, что в нем реализован этот интерфейс.
                            +1
                            Однако те, кто пользовался шаблонами C++ знают, на что могут быть похожи сообщения об ошибках. На что угодно, только не на подсказку
                            Отлично сказано!
                              –1
                              Раньше думал, что темплейты это круто, а дженерики в .Net это ужас, потому что не позволяют написать код в стиле
                              T Sum<T>(T a, T b) where T : operator +
                              {
                                 return a + b;
                              }
                              

                              но когда я вижу, что в С++ только появляется то, что в .Net есть с версии 2.0 десятилетней давности… Не-е-ет, лучше уж то, что имеем :)

                              Надеюсь на постепенное слияние идей дженериков (обобщений) .Net и темплейтов (шаблонов) C++: с одной стороны, поддержка ограничений на этапе компиляции с человеческими сообщениями об ошибках, с другой — возможность сделать обобщение не только на определенные методы, но и на операторы: >, < +, — и пр.
                                0
                                (удалено)

                              Only users with full accounts can post comments. Log in, please.