The examples in this section implement a simple abstraction
present in many programming languages outside the C++ family.
These languages allow
the programmer to specify both an upper and lower bound on the
subscripts for an array.
In this section we implement this five times, in slightly different
ways, but the usage is similar:
BoundArr<int> ba(10,30);
for(int i = ba.low(); i <= ba.high(); ++i)
ba.at(i) = i + 1;
int n = ba.at(20);
The objects are constructed giving both a lower and an upper bound,
and subscripts must be in that range, inclusive. Subscripting is
done with the
at function, which can be used on either side
of the assignment. All the versions use the same internal
data structure, which is just a normal, dynamically-allocated array
of the needed size. The
at method just maps user subscripts to
the zero-based ones by subtracting the lower bound. For instance,
if we declare
BoundArr<int> fred(5,10);, the object
fred
looks something like this:
If I call
fred.at(7), the
at method takes its parameter
and subtracts the object value low to get the private subscript. So the
data the user thinks is in position seven is in position two of the
allocated C++ array. The
at method also checks bounds.
The array space is dynamically allocated by the constructor and referenced by a
smart pointer which frees the space when the object is destroyed.
Miscellaneous New Stuff
The example is constructed to illustrate the copying problem, but
there are a few things in the code that we have not seen before
which are worth mentioning.
One oddity is the placement of method bodies after the
class, but in the same file. The syntax of the provided bodies is much
like what we've seen when providing an implementation file. But this is a
template class, so the bodies must be in the header with the template.
Putting them afterwords is by no means required, but it arguably
makes the code easier to read by separating the class declaration from
the more complicated method bodies.
The declaration of
the
at method may seem strange as well. The declarations are not long:
T &at(int subs) { return m_space[offset(subs)]; }
T const &at(int subs) const { return m_space[offset(subs)] }
The
& at the left is part of the return type and declares
the the function returns a reference.
This allows the
at method
to be used on the left side of an assignment statement.
Using the returned reference to
m_space[offset(subs)] on the left of an
assignment is the same as using
m_space[offset(subs)] on the left of an assignment, so
a.at(i) = 10 assigns to the appropriate position within
m_space.
Another issue here is the overloading. There are two versions of
at, but you notice that they each take a single integer parameter,
which would seem to create an ambiguity.
It happens, however, that C++
includes the const qualifier as part of the signature,
which is why
the two definitions are allowed. When the compiler encounters
x.at(i), it chooses which at to call based on the
presence or absence of a const qualifier on the declaration of
x. If there is none, it uses the first at, otherwise
the second. So, if x is created by a usual declaration like
BoundArray<std::string> x, then x.at(i) uses the
first at; if x is const, for instance a parameter
declared const BoundArray<std::string> & x, then
x.at(i) calls the second at.
This is important for enforcing const,
since using at on the left of an assignment is allowed only
for the first form. Believe it or not, this is a common pattern for
building containers.
Another
novelty appears at the bottom of the header file,
the declaration of the special function
operator<<:
template <typename T>
inline std::ostream & operator<<(std::ostream &strm, const BoundArr<T> &arr)
{
arr.print(strm);
return strm;
}
It allows
BoundArr objects to be printed using
the
iostream facility.
The parameter type
std::ostream is the type
(actually a super-type) of
std::cout. When the compiler
encounters
std::cout << arr where
arr is
a
BoundArr object, the compiler translates that as
operator<<(std::cout, arr)
which runs the function body just as written, being
mostly the call
arr.print(cout). As you can see earlier in
the file,
print is an ordinary object method
which does just what it's name says.
The return value allows for chaining. The expression
std::cout << "The result values are: " << arr << std::endl;
uses the
<< operator which groups left to right, so it is
equivalent to
((std::cout << "The result values are: ") << arr) << std::endl;
Which then translates to
operator<<(operator<<(operator<<(std::cout, "The result values are: "), arr), std::endl)
By returning a reference to the stream, each call to
operator<<
can do its work then
pass the stream on to the next call.