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.
REQUIRE(type, concept); REQUIRE2(type1, type2, concept); REQUIRE3(type1, type2, type3, concept); REQUIRE4(type1, type2, type3, type4, concept); CLASS_REQUIRES(type, concept); CLASS_REQUIRES2(type1, type2, concept); CLASS_REQUIRES3(type1, type2, type3, concept); CLASS_REQUIRES4(type1, type2, type3, type4, concept);To add concept checks to the std::stable_sort() function the library implementor would simply insert REQUIRE 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 REQUIRE to check this.
template <class RandomAccessIter>
void stable_sort(RandomAccessIter first, RandomAccessIter last)
{
REQUIRE(RandomAccessIter, RandomAccessIterator);
typedef typename std::iterator_traits<RandomAccessIter>::value_type value_type;
REQUIRE(value_type, LessThanComparable);
...
}
The Boost Concept Checking library includes a concept checking class
for each of the concepts described in the SGI STL documentation. The
second argument to REQUIRE indicates which concept checking
class should be used. In the above example, the use of
LessThanComparable indicates that the
LessThanComparable_concept class will be used to check the
value type. The Reference section below
lists the concept checking classes.
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 the REQUIRE2 macro 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;
REQUIRE2(ColorMap, Vertex, ReadWritePropertyMap);
...
}
As an example of using the CLASS_REQUIRES macro 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(T, Assignable)
at the top of the definition for std::vector.
namespace std {
template <class T>
struct vector {
CLASS_REQUIRES(T, Assignable);
...
};
}
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() {
REQUIRE(Iter, BidirectionalIterator);
REQUIRE(Iter, LessThanComparable);
REQUIRE2(typename std::iterator_traits<Iter>::iterator_category,
std::random_access_iterator_tag, Convertible);
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
macro named REQUIRE. The following code snippet shows how to
use REQUIRE to make sure that the iterator is a
RandomAccessIterator.
template <class RandomAccessIter>
void stable_sort(RandomAccessIter first, RandomAccessIter last)
{
REQUIRE(RandomAccessIter, RandomAccessIterator);
...
}
The definition of the REQUIRE is as follows. The
type_var is the type we wish to check, and concept
is the name that corresponds to the concept checking class. 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.
#define REQUIRE(type_var, concept) \
do { \
void (concept##_concept <type_var>::*x)() = \
concept##_concept <type_var>::constraints; \
x = x; \
} while (0)
To check the type parameters of class templates, we provide the
CLASS_REQUIRES macro which can be used inside the body of a
class definition (whereas the REQUIRES macro can only be used
inside of a function body). This macro 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. We use the
type_var and concept names in the nested class and
typedef names to help prevent name collisions.
#define CLASS_REQUIRES(type_var, concept) \
typedef void (concept##_concept <type_var> \
::* func##type_var##concept)(); \
\
template <func##type_var##concept FuncPtr> \
struct dummy_struct_##type_var##concept { }; \
\
typedef dummy_struct_##type_var##concept< \
concept##_concept <type_var>::constraints> \
dummy_typedef_##type_var##concept
In addition, there are versions of REQUIRE and
CLASS_REQUIRES that take more arguments, to handle concepts
that include interactions between two or more types.
CLASS_REQUIRES was not used in the implementation of the STL
concept checks because several compilers do not implement template
parameters of function pointer type.
// Apply concept checks in function definitions. REQUIRE(type, concept); REQUIRE2(type1, type2, concept); REQUIRE3(type1, type2, type3, concept); REQUIRE4(type1, type2, type3, type4, concept);
// Apply concept checks in class definitions. CLASS_REQUIRES(type, concept); CLASS_REQUIRES2(type1, type2, concept); CLASS_REQUIRES3(type1, type2, type3, concept); CLASS_REQUIRES4(type1, type2, type3, type4, concept);
// Make sure that type1 and type2 are exactly the same type REQUIRE_SAME_TYPE(type1, type2);
template <class T> struct Integer_concept; // Is T a built-in integer type? template <class T> struct SignedInteger_concept; // Is T a built-in signed integer type? template <class X, class Y> struct Convertible_concept; // Is X convertible to Y? template <class T> struct Assignable_concept; template <class T> struct DefaultConstructible_concept; template <class T> struct CopyConstructible_concept; template <class T> struct Boolean_concept; template <class T> struct EqualityComparable_concept; // Is class T equality comparable on the left side with type Left? template <class T, class Left> struct LeftEqualityComparable_concept; template <class T> struct LessThanComparable_concept;
template <class Iter> struct TrivialIterator_concept; template <class Iter> struct Mutable_TrivialIterator_concept; template <class Iter> struct InputIterator_concept; template <class Iter, class T> struct OutputIterator_concept; template <class Iter> struct ForwardIterator_concept; template <class Iter> struct Mutable_ForwardIterator_concept; template <class Iter> struct BidirectionalIterator_concept; template <class Iter> struct Mutable_BidirectionalIterator_concept; template <class Iter> struct RandomAccessIterator_concept; template <class Iter> struct Mutable_RandomAccessIterator_concept;
template <class Func, class Return> struct Generator_concept;
template <class Func, class Return, class Arg> struct UnaryFunction_concept;
template <class Func, class Return, class First, class Second> struct BinaryFunction_concept;
template <class Func, class Arg> struct UnaryPredicate_concept;
template <class Func, class First, class Second> struct BinaryPredicate_concept;
template <class Func, class First, class Second> struct Const_BinaryPredicate_concept {;
template <class C> struct Container_concept; template <class C> struct Mutable_Container_concept; template <class C> struct ForwardContainer_concept; template <class C> struct Mutable_ForwardContainer_concept; template <class C> struct ReversibleContainer_concept; template <class C> struct Mutable_ReversibleContainer_concept; template <class C> struct RandomAccessContainer_concept; template <class C> struct Mutable_RandomAccessContainer_concept; template <class C> struct Sequence_concept; template <class C> struct FrontInsertionSequence_concept; template <class C> struct BackInsertionSequence_concept; template <class C> struct AssociativeContainer_concept; template <class C> struct UniqueAssociativeContainer_concept; template <class C> struct MultipleAssociativeContainer_concept; template <class C> struct SimpleAssociativeContainer_concept; template <class C> struct PairAssociativeContainer_concept; template <class C> struct SortedAssociativeContainer_concept;
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) |