To understand how to extend regularity to composite objects, let us start with some simple cases. In
Chapter 1 we introduced the type constructor pair, which, given two types T0 and T1, returns the structure type pairT
0, T1. We implement pair with a structure template together with some global procedures:
template<typename T0, typename T1> requires(Regular(T0) && Regular(T1)) struct pair
{
T0 m0; T1 m1;
pair() { } // default constructor
pair(const T0& m0, const T1& m1) : m0(m0), m1(m1) { } };
C++ ensures that the default constructor performs a default construction of both members,
guaranteeing that they are in partially formed states and can thus be assigned to or destroyed. C++ automatically generates a copy constructor and assignment that, respectively, copies or assigns each member and automatically generates a destructor that invokes the destructor for each member. We need to provide equality and ordering manually:
template<typename T0, typename T1> requires(Regular(T0) && Regular(T1))
bool operator==(const pair<T0, T1>& x, const pair<T0, T1>& y) {
return x.m0 == y.m0 && x.m1 == y.m1; }
template<typename T0, typename T1>
requires(TotallyOrdered(T0) && TotallyOrdered(T1)) bool operator<(const pair<T0, T1>& x, const pair<T0, T1>& y) {
return x.m0 < y.m0 || (!(y.m0 < x.m0) && x.m1 < y.m1); }
Exercise 12.1.
Exercise 12.2.
Implement the default ordering, less, for pairT0, T1, using the default orderings for T0 and T1, for situations in which both member types are not totally ordered.
While pair is a heterogeneous type constructor, array_k is a homogeneous type constructor, which, given an integer k and a type T, returns the constant-size sequence type array_kk, T:
template<int k, typename T>
requires(0 < k && k <= MaximumValue(int) / sizeof(T) && Regular(T)) struct array_k { T a[k]; T& operator[](int i) { // Precondition: 0 i < k return a[i]; } };
The requirement on k is defined in terms of type attributes. MaximumValue(N) returns the maximum value representable by the integer type N, and sizeof is the built-in type attribute that returns the size of a type. C++ generates the default constructor, copy constructor, assignment, and destructor for array_k with correct semantics. We implement the member function that allows reading or writing
x[i].[1]
[1] As with begin and end, overloading on constant is needed for a complete implementation.
IteratorType(array_kk, T) is defined to be pointer to T. We provide procedures to return the first and the limit of the array elements:[2]
[2] A complete implementation will also provide a constant iterator type, as a constant pointer to T, together with versions of begin and end overloaded on constant array_k that return the constant iterator type.
template<int k, typename T> requires(Regular(T))
pointer(T) begin(array_k<k, T>& x) {
return addressof(x.a[0]); }
template<int k, typename T> requires(Regular(T))
pointer(T) end(array_k<k, T>& x) {
return addressof(x.a[k]); }
An object x of array_kk, T type can be initialized to a copy of the counted range with code like
copy_n(f, k, begin(x));
We do not know how to implement a proper initializing constructor that avoids the automatically generated default construction of every element of the array. In addition, while copy_n takes any category of iterator and returns the limit iterator, there would be no way to return the limit iterator from a copy constructor.
Equality and ordering for arrays use the lexicographical extensions introduced in Chapter 7: Implement tripleT
template<int k, typename T> requires(Regular(T))
bool operator==(const array_k<k, T>& x, const array_k<k, T>& y) {
return lexicographical_equal(begin(x), end(x), begin(y), end(y)); }
template<int k, typename T> requires(Regular(T))
bool operator<(const array_k<k, T>& x, const array_k<k, T>& y) {
return lexicographical_less(begin(x), end(x), begin(y), end(y)); }
Exercise 12.3.
Exercise 12.4.
We provide a procedure to return the number of elements in the array:
template<int k, typename T> requires(Regular(T))
int size(const array_k<k, T>& x) {
return k; }
and one to determine whether the size is 0:
template<int k, typename T> requires(Regular(T))
bool empty(const array_k<k, T>& x) {
return false; }
We took the trouble to define size and empty so that array_k would model Sequence, which we define later.
Exercise 12.5.
array_k models the concept Linearizable:
Implement versions of = and < for array_kk, T that generate inline unrolled code for small k.
Implement the default ordering, less, for array_kk, T.
Extend array_k to accept k = 0.
empty always takes constant time, even when size takes linear time. The precondition for w[i] is 0 i size(w); its complexity is determined by the iterator type specification of concepts refining Linearizable: linear for forward and bidirectional iterators and constant for indexed and random- access iterators.
A linearizable type describes a range of iterators via the standard functions begin and end, but unlike array_k, copying a linearizable does not need to copy the underlying objects; as we shall see later, it is not a container, a sequence that owns its elements. The following type, for example, models Linearizable and is not a container; it designates a bounded range of iterators residing in some data structure:
template<typename I>
requires(Readable(I) && Iterator(I)) struct bounded_range {
I f; I l;
bounded_range() { }
bounded_range(const I& f, const I& l) : f(f), l(l) { } const ValueType(I)& operator[](int i)
{
// Precondition: 0 i < l – f
return source(f + i); }
};
C++ automatically generates the copy constructor, assignment, and destructor, with the same semantics as pairI, I. If T is bounded_rangeI, IteratorType(T) is defined to be I, and SizeType(T) is defined to be DistanceType(I).
It is straightforward to define the iterator-related procedures:
Regular(W)
IteratorType: Linearizable Iterator
ValueType: Linearizable Regular
W ValueType(IteratorType(W))
SizeType: Linearizable Integer
W DistanceType(IteratorType(W)) begin: W IteratorType(W) end: W IteratorType(W) size: W SizeType(W) x end(x) – begin(x) empty: W bool x begin(x) = end(x)
[ ]: W x SizeType(W) ValueType(W)& (w, i) deref(begin(w)+ i)
template<typename I>
requires(Readable(I) && Iterator(I))
I begin(const bounded_range<I>& x) { return x.f; } template<typename I>
requires(Readable(I) && Iterator(I))
I end(const bounded_range<I>& x) { return x.l; } template<typename I>
requires(Readable(I) && Iterator(I))
DistanceType(I) size(const bounded_range<I>& x) {
return end(x) - begin(x); }
template<typename I>
requires(Readable(I) && Iterator(I)) bool empty(const bounded_range<I>& x) {
return begin(x) == end(x); }
Unlike array_k, equality for bounded_range does not use lexicographic equality but instead effectively treats the object as a pair of iterators and compares the corresponding values:
template<typename I>
requires(Readable(I) && Iterator(I)) bool operator==(const bounded_range<I>& x, const bounded_range<I>& y) {
return begin(x) == begin(y) && end(x) == end(y); }
The equality so defined is consistent with the copy constructor generated by C++, which treats it just as a pair of iterators. Consider a type W that models Linearizable. If W is a container with linear coordinate structure, lexicographical_equal is its correct equality, as we defined for array_k. If W is a homogeneous container whose coordinate structure is not linear (e.g., a tree or a matrix), neither lexicographical_equal nor range equality (as we defined for bounded_range) is the correct
equality, although lexicographical_equal may still be a useful algorithm. If W is not a container but just a description of a range owned by another data structure, range equality is its correct equality.
The default total ordering for bounded_range
I is defined lexicographically on the pair of iterators, using the default total ordering for I:
template<typename I>
requires(Readable(I) && Iterator(I)) struct less< bounded_range<I> >
{
bool operator()(const bounded_range<I>& x, const bounded_range<I>& y) {
less<I> less_I;
return less_I(begin(x), begin(y)) || (!less_I(begin(y), begin(x)) && less_I(end(x), end(y))); }
};
Even when an iterator type has no natural total ordering, it should provide a default total ordering: for example, by treating the bit pattern as an unsigned integer.
object if it is made up of other objects, called its parts. The whole–part relationship satisfies the four properties of connectedness, noncircularity, disjointness, and ownership. Connectedness means that an object has an affiliated coordinate structure that allows every part of the object to be reached from the object's starting address. Noncircularity means that an object is not a subpart of itself, where subparts of an object are its parts and subparts of its parts. (Noncircularity implies that no object is a part of itself.) Disjointness means that if two objects have a subpart in common, one of the two is a subpart of the other. Ownership means that copying an object copies its parts, and destroying the object destroys its parts. A composite object is dynamic if the set of its parts could change over its lifetime.
We refer to the type of a composite object as a composite object type and to a concept modeled by a composite object type as a composite object concept. No algorithms can be defined on composite objects as such, since composite object is a concept schema rather than a concept.