Generic programming in C++ is characterized by the use of template parameters to represent abstract data types (or ``concepts''). However, the C++ language itself does not provide a mechanism for explicitly handling concepts. As a result, it can be difficult to insure that a concrete type meets the requirements of the concept it is supposed to represent. Error messages resulting from incorrect use of a concrete type can be particularly difficult to decipher. The Boost Concept Checking Library provides mechanisms for checking parameters in C++ template libraries. The mechanisms use standard C++ and introduce no run-time overhead. The main cost of using the mechanism is in compile-time. The documentation is organized into the following sections.
Jeremy Siek contributed this library. X managed the formal review.
Naturally, if a generic algorithm is invoked with a type that does not fulfill at least the syntactic requirements of the concept, a compile-time error will occur. However, this error will not per se reflect the fact that the type did not meet all of the requirements of the concept. Rather, the error may occur deep inside the instantiation hierarchy at the point where an expression is not valid for the type, or where a presumed associated type is not available. The resulting error messages are largely uninformative and basically impenetrable.
What is required is a mechanism for enforcing ``concept safety'' at (or close to) the point of instantiation. The Boost Concept Checking Library uses some standard C++ constructs to enforce early concept compliance and that provides more informative error messages upon non-compliance.
Note that this technique only addresses the syntactic requirements of concepts (the valid expressions and associated types). We do not address the semantic invariants or complexity guarantees, which are also part of concept requirements..
bad_error_eg.cpp:
1 #include <list>
2 #include <algorithm>
3
4 struct foo {
5 bool operator<(const foo&) const { return false; }
6 };
7 int main(int, char*[]) {
8 std::list<foo> v;
9 std::stable_sort(v.begin(), v.end());
10 return 0;
11 }
Here, the
std::stable_sort() algorithm is prototyped as follows:
template <class RandomAccessIterator> void stable_sort(RandomAccessIterator first, RandomAccessIterator last);Attempting to compile this code with Gnu C++ produces the following compiler error. The output from other compilers is listed in the Appendix.
stl_algo.h: In function `void __merge_sort_loop<_List_iterator <foo,foo &,foo *>, foo *, int>(_List_iterator<foo,foo &,foo *>, _List_iterator<foo,foo &,foo *>, foo *, int)': stl_algo.h:1448: instantiated from `__merge_sort_with_buffer <_List_iterator<foo,foo &,foo *>, foo *, int>( _List_iterator<foo,foo &,foo *>, _List_iterator<foo,foo &,foo *>, foo *, int *)' stl_algo.h:1485: instantiated from `__stable_sort_adaptive< _List_iterator<foo,foo &,foo *>, foo *, int>(_List_iterator <foo,foo &,foo *>, _List_iterator<foo,foo &,foo *>, foo *, int)' stl_algo.h:1524: instantiated from here stl_algo.h:1377: no match for `_List_iterator<foo,foo &,foo *> & - _List_iterator<foo,foo &,foo *> &'In this case, the fundamental error is that std:list::iterator does not model the concept of RandomAccessIterator. The list iterator is only bidirectional, not fully random access (as would be a vector iterator). Unfortunately, there is nothing in the error message to indicate this to the user.
To a C++ programmer having enough experience with template libraries the error may be obvious. However, for the uninitiated, there are several reasons why this message would be hard to understand.
concept_checks.hpp: In method `void LessThanComparable_concept <_List_iterator<foo,foo &,foo *> >::constraints()': concept_checks.hpp:334: instantiated from `RandomAccessIterator_concept <_List_iterator<foo,foo &,foo *> >::constraints()' bad_error_eg.cpp:9: instantiated from `stable_sort<_List_iterator <foo,foo &,foo *> >(_List_iterator<foo,foo &,foo *>, _List_iterator<foo,foo &,foo *>)' concept_checks.hpp:209: no match for `_List_iterator<foo,foo &,foo *> & < _List_iterator<foo,foo &,foo *> &'This message rectifies several of the shortcomings of the standard error messages.
template <class T> struct EqualityComparableConcept;Each concept checking class has a member function named constraints() which contains the valid expressions for the concept. To check whether some type, say foo, is EqualityComparable, we need to instantiate the concept checking class with foo: EqualityComparableConcept<foo> and then find a way to get the compiler to compile the constraints() function without actually calling it. The Boost Concept Checking Library defines two utilities that make this easy: function_requires() and BOOST_CLASS_REQUIRES. function_requires() function can be used in function bodies and the BOOST_CLASS_REQUIRES macro can be used inside class bodies. The function_requires() function takes no arguments, but has a template parameter for the concept checking class. This means that the instantiated concept checking class must be given as an explicit template argument, as shown below.
void some_function_using_foo() {
function_requires< EqualityComparableConcept<foo> >();
// ...
};
The BOOST_CLASS_REQUIRES macro can be used inside a class
definition to check whether some type models a concept.
struct some_class_using_foo {
BOOST_CLASS_REQUIRES(foo, EqualityComparableConcept);
};
To add concept checks to the std::stable_sort() function the
library implementor would simply insert function_requires()
at the top of std::stable_sort() to make sure the template
parameter type models
RandomAccessIterator. In addition, std::stable_sort()
requires that the value_type of the iterators be
LessThanComparable, so we also use function_requires() to
check this.
template <class RandomAccessIter>
void stable_sort(RandomAccessIter first, RandomAccessIter last)
{
function_requires< RandomAccessIteratorConcept<RandomAccessIter> >();
typedef typename std::iterator_traits<RandomAccessIter>::value_type value_type;
function_requires< LessThanComparableConcept<value_type> >();
...
}
Some concepts deal with more than one type. In this case the corresponding concept checking class will have multiple template parameters. The following example shows how function_requires() is used with the ReadWritePropertyMap concept which takes two type parameters: a property map and the key type for the map.
template <class IncidenceGraph, class Buffer, class BFSVisitor,
class ColorMap>
void breadth_first_search(IncidenceGraph& g,
typename graph_traits<IncidenceGraph>::vertex_descriptor s,
Buffer& Q, BFSVisitor vis, ColorMap color)
{
typedef typename graph_traits<IncidenceGraph>::vertex_descriptor Vertex;
function_requires< ReadWritePropertyMap<ColorMap, Vertex> >();
...
}
As an example of using class_requires we look at a concept
check that could be added to std::vector. One requirement
that is placed on the element type is that it must be Assignable.
We can check this by inserting
class_requires<AssignableConcept<T> > at the top
of the definition for std::vector.
namespace std {
template <class T>
struct vector {
typedef typename class_requires< AssignableConcept<T> >::check req;
...
};
}
Although the concept checks are designed for use by generic library
implementors, they can also be useful to end users. Sometimes one may
not be sure whether some type models a particular concept. This can
easily be checked by creating a small program and using
function_requires() with the type and concept in question.
The file stl_concept_checks.cpp
gives and example of applying the concept checks to STL
containers. The file is listed here:
#include <boost/concept_checks.hpp>
#include <iterator>
#include <set>
#include <map>
#include <vector>
#include <list>
#include <deque>
int
main()
{
typedef std::vector<int> Vector;
typedef std::deque<int> Deque;
typedef std::list<int> List;
function_requires< Mutable_RandomAccessContainer<Vector> >();
function_requires< BackInsertionSequence<Vector> >();
function_requires< Mutable_RandomAccessContainer<Deque> >();
function_requires< FrontInsertionSequence<Deque> >();
function_requires< BackInsertionSequence<Deque> >();
function_requires< Mutable_ReversibleContainer<List> >();
function_requires< FrontInsertionSequence<List> >();
function_requires< BackInsertionSequence<List> >();
typedef std::set<int> Set;
typedef std::multiset<int> MultiSet;
typedef std::map<int,int> Map;
typedef std::multimap<int,int> MultiMap;
function_requires< SortedAssociativeContainer<Set> >();
function_requires< SimpleAssociativeContainer<Set> >();
function_requires< UniqueAssociativeContainer<Set> >();
function_requires< SortedAssociativeContainer<MultiSet> >();
function_requires< SimpleAssociativeContainer<MultiSet> >();
function_requires< MultipleAssociativeContainer<MultiSet> >();
function_requires< SortedAssociativeContainer<Map> >();
function_requires< UniqueAssociativeContainer<Map> >();
function_requires< PairAssociativeContainer<Map> >();
function_requires< SortedAssociativeContainer<MultiMap> >();
function_requires< MultipleAssociativeContainer<MultiMap> >();
function_requires< PairAssociativeContainer<MultiMap> >();
return 0;
}
The first part of the constraints() function includes the requirements that correspond to the refinement relationship between RandomAccessIterator and the concepts which it builds upon: BidirectionalIterator and LessThanComparable. We could have instead used CLASS_REQUIRES and placed these requirements in the class body, however CLASS_REQUIRES uses C++ language features that are less portable.
Next we check that the iterator_category of the iterator is either std::random_access_iterator_tag or a derived class. After that we write out some code that corresponds to the valid expressions of the RandomAccessIterator concept. Typedefs can also be added to enforce the associated types of the concept.
template <class Iter>
struct RandomAccessIterator_concept
{
void constraints() {
function_requires< BidirectionalIteratorConcept<Iter> >();
function_requires< LessThanComparableConcept<Iter> >();
function_requires< ConvertibleConcept<
typename std::iterator_traits<Iter>::iterator_category,
std::random_access_iterator_tag> >();
i += n;
i = i + n; i = n + i;
i -= n;
i = i - n;
n = i - j;
i[n];
}
Iter a, b;
Iter i, j;
typename std::iterator_traits<Iter>::difference_type n;
};
}
One potential pitfall in designing concept checking classes is using
more expressions in the constraint function than necessary. For
example, it is easy to accidentally use the default constructor to
create the objects that will be needed in the expressions (and not all
concepts require a default constructor). This is the reason we write
the constraint function as a member function of a class. The objects
involved in the expressions are declared as data members of the class.
Since objects of the constraints class template are never
instantiated, the default constructor for the concept checking class
is never instantiated. Hence the data member's default constructors
are never instantiated (C++ Standard Section 14.7.1 9).
template <class T>
struct input_proxy {
operator T() { return t; }
static T t;
};
template <class T>
class trivial_iterator_archetype
{
typedef trivial_iterator_archetype self;
public:
trivial_iterator_archetype() { }
trivial_iterator_archetype(const self&) { }
self& operator=(const self&) { return *this; }
friend bool operator==(const self&, const self&) { return true; }
friend bool operator!=(const self&, const self&) { return true; }
input_proxy<T> operator*() { return input_proxy<T>(); }
};
namespace std {
template <class T>
struct iterator_traits< trivial_iterator_archetype<T> >
{
typedef T value_type;
};
}
Generic algorithms are often tested by being instantiated with a
number of common input types. For example, one might apply
std::stable_sort() with basic pointer types as the iterators.
Though appropriate for testing the run-time behavior of the algorithm,
this is not helpful for ensuring concept coverage because C++ types
never match particular concepts, they often provide much more than the
minimal functionality required by any one concept. That is, even
though the function template compiles with a given type, the concept
requirements may still fall short of covering the functions actual
requirements. This is why it is important to compile with archetype
classes in addition to testing with common input types.
The following is an excerpt from stl_concept_covering.cpp that shows how archetypes can be used to check the requirement documentation for std::stable_sort(). In this case, it looks like the CopyConstructible and Assignable requirements were forgotten in the SGI STL documentation (try removing those archetypes). The Boost archetype classes have been designed so that they can be layered. In this example the value type of the iterator is composed out of three archetypes. In the archetype class reference below, template parameters named Base indicate where the layered archetype can be used.
{
typedef less_than_comparable_archetype<
copy_constructible_archetype<
assignable_archetype<> > > ValueType;
random_access_iterator_archetype<ValueType> ri;
std::stable_sort(ri, ri);
}
Requirement Minimization Principle: Minimize the requirements on the input parameters of a component to increase its reusability.
There is natural tension in this statement. By definition, the input parameters must be used by the component in order for the component to accomplish its task (by ``component'' we mean a function or class template). The challenge then is to implement the component in such a way that makes the fewest assumptions (the minimum requirements) about the inputs while still accomplishing the task.
The traditional notions of abstraction tie in directly to the idea of minimal requirements. The more abstract the input, the fewer the requirements. Thus, concepts are simply the embodiment of generic abstract data types in C++ template programming.
When designing the concepts for some problem domain it is important to keep in mind their purpose, namely to express the requirements for the input to the components. With respect to the requirement minimization principle, this means we want to minimize concepts.
It is important to note, however, that minimizing concepts does not mean simply reducing the number of valid expressions in the concept. For example, the std::stable_sort() function requires that the value type of the iterator be LessThanComparable, which not only includes operator<(), but also operator>(), operator<=(), and operator>=(). It turns out that std::stable_sort() only uses operator<(). The question then arises: should std::stable_sort() be specified in terms of the concept LessThanComparable or in terms of a concept that only requires operator<()?
We remark first that the use of LessThanComparable does not really violate the requirement minimization principle because all of the other operators can be trivially implemented in terms of operator<(). By ``trivial'' we mean one line of code and a constant run-time cost. More fundamentally, however, the use of LessThanComparable does not violate the requirement minimization principle because all of the comparison operators (<, >, <=, >=) are conceptually equivalent (in a mathematical sense). Adding conceptually equivalent valid expressions is not a violation of the requirement minimization principle because no new semantics are being added --- only new syntax. The added syntax increases re-usability.
For example, the maintainer of the std::stable_sort() may some day change the implementation in places to use operator>() instead of operator<(), since, after all, they are equivalent. Since the requirements are part of the public interface, such a change could potentially break client code. If instead LessThanComparable is given as the requirement for std::stable_sort(), then the maintainer is given a reasonable amount of flexibility within which to work.
Minimality in concepts is a property associated with the underlying semantics of the problem domain being represented. In the problem domain of basic containers, requiring traversal in a single direction is a smaller requirement than requiring traversal in both directions (hence the distinction between ForwardIterator and BidirectionalIterator). The semantic difference can be easily seen in the difference between the set of concrete data structures that have forward iterators versus the set that has bidirectional iterators. For example, singly-linked lists would fall in the set of data structures having forward iterators, but not bidirectional iterators. In addition, the set of algorithms that one can implement using only forward iterators is quite different than the set that can be implemented with bidirectional iterators. Because of this, it is important to factor families of requirements into rather fine-grained concepts. For example, the requirements for iterators are factored into the six STL iterator concepts (trivial, output, input, forward, bidirectional, and random access).
template <class RandomAccessIterator>
void stable_sort_constraints(RandomAccessIterator i) {
typename std::iterator_traits<RandomAccessIterator>
::difference_type n;
i += n; // exercise the requirements for RandomAccessIterator
...
}
template <class RandomAccessIterator>
void stable_sort(RandomAccessIterator first, RandomAccessIterator last) {
typedef void (*fptr_type)(RandomAccessIterator);
fptr_type x = &stable_sort_constraints;
...
}
There is often a large set of requirements that need to be checked,
and it would be cumbersome for the library implementor to write
constraint functions like stable_sort_constraints() for every
public function. Instead, we group sets of valid expressions
together, according to the definitions of the corresponding concepts.
For each concept we define a concept checking class template where the
template parameter is for the type to be checked. The class contains
a contraints() member function which exercises all of the
valid expressions of the concept. The objects used in the constraints
function, such as n and i, are declared as data
members of the concept checking class.
template <class Iter>
struct RandomAccessIterator_concept {
void constraints() {
i += n;
...
}
typename std::iterator_traits<RandomAccessIterator>
::difference_type n;
Iter i;
...
};
We can still use the function pointer mechanism to cause instantiation
of the constraints function, however now it will be a member function
pointer. To make it easy for the library implementor to invoke the
concept checks, we wrap the member function pointer mechanism in a
function named function_requires(). The following code
snippet shows how to use function_requires() to make sure
that the iterator is a
RandomAccessIterator.
template <class RandomAccessIter>
void stable_sort(RandomAccessIter first, RandomAccessIter last)
{
function_requires< RandomAccessIteratorConcept >();
...
}
The definition of the function_requires() is as follows. The
Concept is the concept checking class that has been
instantiated with the modeling type. We assign the address of the
constraints member function to the function pointer x, which
causes the instantiation of the constraints function and checking of
the concept's valid expressions. We then assign x to
x to avoid unused variable compiler warnings, and wrap
everything in a do-while loop to prevent name collisions.
templateTo check the type parameters of class templates, we provide the class_requires class which can be used inside the body of a class definition (whereas function_requires() can only be used inside of a function body). class_requires declares a nested class template, where the template parameter is a function pointer. We then use the nested class type in a typedef with the function pointer type of the constraint function as the template argument.void function_requires() { void (Concept::*x)() = BOOST_FPTR Concept::constraints; ignore_unused_variable_warning(x); }
template <class Concept>
class class_requires
{
typedef void (Concept::* function_pointer)();
template <function_pointer Fptr>
struct dummy_struct { };
public:
typedef dummy_struct< BOOST_FPTR Concept::constraints > check;
};
class_requires was not used in the implementation of the
Boost Concept Checking Library concept checks because several
compilers do not implement template parameters of function pointer
type.
template <class Concept> void function_requires();
template <class Concept>
struct class_requires {
typedef ... check;
};
// Make sure that Type1 and Type2 are exactly the same type.
// If they are not, then the nested typedef for type will
// not exist and cause a compiler error.
template <class Type1, class Type2>
struct require_same {
typedef ... type;
};
// usage example
typedef typedef require_same::type req1; // this will compile OK
typedef typedef require_same::type req1; // this will cause a compiler error
template <class T> struct Integer_concept; // Is T a built-in integer type? template <class T> struct SignedIntegerConcept; // Is T a built-in signed integer type? template <class X, class Y> struct ConvertibleConcept; // Is X convertible to Y? template <class T> struct AssignableConcept; template <class T> struct DefaultConstructibleConcept; template <class T> struct CopyConstructibleConcept; template <class T> struct BooleanConcept; template <class T> struct EqualityComparableConcept; // Is class T equality comparable on the left side with type Left? template <class T, class Left> struct LeftEqualityComparableConcept; template <class T> struct LessThanComparableConcept;
template <class Iter> struct TrivialIteratorConcept; template <class Iter> struct Mutable_TrivialIteratorConcept; template <class Iter> struct InputIteratorConcept; template <class Iter, class T> struct OutputIteratorConcept; template <class Iter> struct ForwardIteratorConcept; template <class Iter> struct Mutable_ForwardIteratorConcept; template <class Iter> struct BidirectionalIteratorConcept; template <class Iter> struct Mutable_BidirectionalIteratorConcept; template <class Iter> struct RandomAccessIteratorConcept; template <class Iter> struct Mutable_RandomAccessIteratorConcept;
template <class Func, class Return> struct GeneratorConcept;
template <class Func, class Return, class Arg> struct UnaryFunctionConcept;
template <class Func, class Return, class First, class Second> struct BinaryFunctionConcept;
template <class Func, class Arg> struct UnaryPredicateConcept;
template <class Func, class First, class Second> struct BinaryPredicateConcept;
template <class Func, class First, class Second> struct Const_BinaryPredicateConcept {;
template <class C> struct ContainerConcept; template <class C> struct Mutable_ContainerConcept; template <class C> struct ForwardContainerConcept; template <class C> struct Mutable_ForwardContainerConcept; template <class C> struct ReversibleContainerConcept; template <class C> struct Mutable_ReversibleContainerConcept; template <class C> struct RandomAccessContainerConcept; template <class C> struct Mutable_RandomAccessContainerConcept; template <class C> struct SequenceConcept; template <class C> struct FrontInsertionSequenceConcept; template <class C> struct BackInsertionSequenceConcept; template <class C> struct AssociativeContainerConcept; template <class C> struct UniqueAssociativeContainerConcept; template <class C> struct MultipleAssociativeContainerConcept; template <class C> struct SimpleAssociativeContainerConcept; template <class C> struct PairAssociativeContainerConcept; template <class C> struct SortedAssociativeContainerConcept;
class null_archetype; // A type that models no concepts. template <class Base = null_archetype> class default_constructible_archetype; template <class Base = null_archetype> class assignable_archetype; template <class Base = null_archetype> class copy_constructible_archetype; template <class Left, class Base = null_archetype> class left_equality_comparable_archetype; template <class Base = null_archetype> class equality_comparable_archetype; template <class T, class Base = null_archetype> class convertible_to_archetype;
template <class ValueType> class trivial_iterator_archetype; template <class ValueType> class mutable_trivial_iterator_archetype; template <class ValueType> class input_iterator_archetype; template <class ValueType> class forward_iterator_archetype; template <class ValueType> class bidirectional_iterator_archetype; template <class ValueType> class random_access_iterator_archetype;
template <class Arg, class Return> class unary_function_archetype; template <class Arg1, class Arg2, class Return> class binary_function_archetype; template <class Arg> class predicate_archetype; template <class Arg1, class Arg2> class binary_predicate_archetype;
UNDER CONSTRUCTION
| Copyright © 2000 | Jeremy Siek, Univ.of Notre Dame (jsiek@lsc.nd.edu) |