Programming with Concepts

The process of deciding how to group requirements into concepts and deciding which concepts to use in each algorithm is perhaps the most difficult (yet most important) part of building a generic library. A guiding principle to use during this process is one we call the requirement minimization principle.

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).