AFF --- A container for numbers (array) by Friederich and Forbriger.
|
Contents of this page:
The class library is intended to be a light-weight library. This means it should offer basic functionality in terms of multidimensional containers with counted references (and not more in first place). We do not like to include a tremendous amount of code for specialized concepts (like subranges or expression templates in Blitz++) each time we just need a small array. Thus the header files providing array declarations (aff/array.h and the files included therein) should be as sparse as possible. All extra functionality like iterators (aff::Iterator presented in aff/iterator.h) or slices (aff::Slice presented in aff/slice.h) should be external to the aff::Array class. This allows us to load their definitions only where needed. However, this approach requires that the internals of aff::Array are exposed to the outside through appropriate functions (see Accessing internals).
Class templates like aff::Iterator may be used with any container class, that provides an appropriate interface. This interface convention concerns the access to the type of related objects. I will explain by example:
We use an iterator i
which was declared
for a container of type Cont
, it expects to find a corresponding container class that promises constness of the elements through
or short
For aff::ConstArray the type aff::ConstArray::Tcoc is just the class itself. However aff::Array::Tcoc gives an aff::ConstArray.
In the same way we may access the appropriate element type through
which is T
for aff::Array<T> and const
for aff::ConstArray<T>. However a
will always provide a type with const qualifier.
In the same way we may access the type of the appropriate representation by
Notice: Using these typedefs (and also the typedefs for the shape class, etc.) improves the maintainability of your code. Think of using the $HOME variable in shell scripts. Once the name of your home directory changes, you need not modify all your shell scripts. Now consider one day your shape class might be renamed...
Tcontainer
and an appropriate conversion operator.Providing extended functionality outside of aff::Array (see Sparse interfaces) requires, that aff::Array, aff::ConstArray, aff::Series, and aff::ConstSeries expose some of their internals. This concerns the underlying shape as well as the represented data.
aff::ConstArray and aff::ConstSeries provide a read-only reference to the data (i.e. an aff::ConstSharedHeap object) through their member-functions aff::ConstArray::representation and aff::ConstSeries::representation, respectively. In the same way aff::Array and aff::Series return an aff::SharedHeap through their representation member function.
All of them return a copy of their shape through the member functions aff::Array::shape, aff::ConstArray::shape, aff::Series::shape, and aff::ConstSeries::shape, respectively. The type of the appropriate shape is available through a member typedef (see Member typedefs).
In return all containers provide a constructor that takes a representation and a shape object and checks for their consistency.
This library contains different classes that provide common interfaces. For example all aff::ConstArray, aff::Array, aff::Series and aff::ConstSeries provide the necessary interface to be used together with aff::Iterator or aff::Browser. A rather elegant way to express this commonality in a template context is the Barton and Nackman trick. All containers that can work together with aff::Iterater sould have to inherit from a class aff::Iteratable. The base class is templated, takes the iteratable class as template parameter and stores a reference to the instance of the iteratable class. This way each iteratable class can be converted to aff::Iteratable, which again returns a reference to the classes iteratable features in the appropriate context.
This way of expressing common interfaces makes the whole classes more complicated than necessary to provide their elementary functionality. We have to store an extra reference to the leaf class object for each feature, we will express this way. And we have to include a whole bunsch of extra code for each feature. Since we prefer Sparse interfaces this method was rejected.
One container class like aff::Array or aff::Series is made up from its class definition together with two other classes like the representation in aff::SharedHeap and a shape like aff::Strided or aff::LinearShape. Why not put all this functionality within one class like aff::Array?
Containers like aff::Array rely on functionality provided by other classes. They are based on shapes like aff::Strided and memory representations like aff::SharedHeap (see The concept of represented memory).
aff::Array has a member of type aff::SharedHeap (which is exposed to the outside through aff::Array::representation), which itself inherits from aff::ConstSharedHeap. At the same time aff::ConstArray has a member of type aff::Array and inherits itself from aff::ConstSharedHeap (which is exposed to the outside through aff::ConstArray::representation). Thus the class aff::ConstSharedHeap is replicated in aff::Array and it is not replicated by deriving from virtual base classes a virtual base class.
The same applies to aff::Series and aff::ConstSeries.
Having an array object a
declared
where T
is any type, we want to pass this object to a function that promises constness of the elements (see Notes on the const-correctness of arrays). The function is thus declared
and we want to use it like
Consequently we must offer a way to convert an
to an
implicitely. This is done by deriving aff::Array<T> publicly from aff::ConstArray<T>.
The memory representation is needed by both, aff::Array<T> and its base class. Hence aff::ConstArray<T> has to inherit from the representation. It would be natural for aff::ConstArray<T> to inherit from aff::ConstSharedHeap only. However, since the derived aff::Array<T> needs full access to an aff::SharedHeap<T> (to expose the representation to the outside), we might tend to derive aff::ConstArray<T> from aff::SharedHeap<T> privately, allowing only read access and conversion to aff::ConstSharedHeap.
Why is this a problem? Consider the inside of the above function. We might know, that the columns of the passed array contain seismogram waveforms. And we might like to access them in an appropriate way (i.e. through an interface that provides waveform operations), though just reading - not modifying - the data. Then we would like to code something like
The above example requires that we can construct an aff::ConstSeries<T> from an aff::ConstSharedHeap<T> (which is returned by aff::ConstArray::representation). The same problem appears together with aff::ConstArray, when creating a subarray or slice from an aff::ConstArray with aff::subarray or aff::slice and aff::ConstArray itself knowing nothing about slices, etc.
Constructing aff::ConstArray from an aff::ConstSharedHeap sounds a natural operation. However, aff::ConstArray will ask for an aff::SharedHeap, if we derive from aff::SharedHeap (as sketched above). Conclusion: aff::ConstArray must use an aff::ConstSharedHeap only. At the same time we must hold the full aff::SharedHeap together with the aff::Array object, since this must return an aff::SharedHeap through aff::Array::representation to allow the above operation (accessing data through aff::Series or constructing a slice - see Sparse interfaces).
The most convincing solution (IMHO) to this problem is to use an (additional) member of type aff::SharedHeap<T> in aff::Array<T> which inherits from aff::ConstArray<T>. In consequence aff::ConstSharedHeap<T> is then a replicated within aff::Array<T>. For a proper design we might consider to make aff::ConstSharedHeap a virtual base, thus avoiding member data duplication. This would, however, introduce an extra level of indirection (additional to the indirection when accessing the heap data through the pointer to the aff::util::SHeap struct in aff::ConstSharedHeap). On the other hand, fully replicating the base aff::ConstSharedHeap just adds one member data pointer (the pointer to the aff::util::SHeap struct) to the data block in aff::Array (which already contains many bytes from the aff::Strided base). This overhead is not considered significant.
But notice: We now must take care to synchronize the aff::SharedHeap base of aff::Array and the aff::ConstSharedHeap base of aff::ConstArray during construction. This is no major concern, but it is error-prone to some degree. It is, however, much easier to keep them synchronous when using member data instead of inheritance.
Usually we would expect the copy operator and the copy constructor to have the same semantics. Here the copy constructor of aff::Array must have reference semantics (it does a shallow copy). This is necessary to allow arrays as return values from functions. In this case the copy constructor is automatically invoked. Reference semantics ensure a minimal overhead. in terms of memory usage and execution time.
In the case of the copy (assignment) operator things are less clear: If we define the copy operator to have reference semantics, it has the same behaviour as the copy constructor. That is what we usually would expect. An expression like
means that array A
drops its reference to the memory location it was pointing to and forgets its previous shape. Following this statement array A
will refer to the same memory location as array B
and will have the same shape. Both are indistinguishable.
However, in many cases (most cases?) we will use the copy (assignment) operator in the sense of a mathematical equation. This may read like
although expressions like this are not yet supported by the library features. In this case we do not mean that A
should drop it reference. A
may refer to an array in memory which is also referred by other array instances. And we want these values to be set to the result of the operation B
+ C
. In that case the copy operator should have deep copy semantics.
We group all code in two namespaces. Major modules which will be accessed by the user are placed in namepsace aff. Modules meant to be used internally are placed in aff::util. Use directives like
or
for convenient access.
When passing a container (i.e. an array) to a function, we would like to promise that the values in the container are not modified, in case the function uses only read-access. Consider a declaration
of a function that takes and argument of type int
an promises that this will not be modified. Passing by reference is used, because this is more efficient than passing by value (in particular for large objects - which is not the case for int
, but for an array). And qualifying the type const
promises that the value passed by reference will not be changed.
A declaration
does not what we want (see General considerations). It just promises the constness of the container, not of the data. Within the function the passed reference may be assigned to a non-const Array<int>
, which allows modification of the data (see The concept of represented memory).
Thus we must use something like
where ConstArray<int>
does not allow modification of the data (be no means - copying and conversions included) and may be derived from an Array<int>
by a trivial conversion (like a conversion to a public base class).
We distinguish between the constness of the array and the constness of the elements. A definition
means that array B
is a constant array initialized to array A
. This means, that the container is constant. Its shape and reference may not be changed.
If you want to define constness of the contained values (e.g. when passing an array to a function), you have to use
which defines that the contents of C
may not be changed (i.e. they are of type const
int
. They are still refering to the same data in memory. If you modify data elements through A
, this will be visible through C
.
An array for elements of type T
is derived from an array for elements of type const
T
. Functions that only need read access to arrays should be declared like
and may be called like
The type conversion from
to
is trivial and has no runtime overhead.
Each container class must deal with this issue on its own. Sorry...
c
that was declared To ensure true constness of the data, you have to assign to the base class of the container. Any container class (e.g. Cont
) provides the type of container for const elements through a typedef Tcontainer_of_const (i.e. Cont::Tcontainer_of_const
) or short Tcoc. Remember that a const
aff::Array
always may be assigned to a mutable aff::Array, which in turn allows modification of the data!
Three alternatives to this concept have been discussed (and discarded). Both have the appealing property of needing only one class definition for each container (in contrast to a class and a base class in our case). Additionally both would offer name commonality for containers of non-const elements and containers of const elements.
const
T
where an array of elements of type T
should be used, that we do not allow to be changed. This design concept can be accomplished with a special traits class that is specialized for const
T
and allows to derive a mutable or const version of any type. By further providing appropriate conversion operators, anArrays using the shared heap representation have reference semantics. Different instances will access the same data in memory. Copying an array instance just copies a reference to the data. This mechanism is not obvious to the compiler. The array instances are values in the sense of C++ types and not references. Passing an const
aff::Array
to a function does not prohibit the function from assigning this instance to a non-const aff::Array
, which then references the same memory area and allows the modification of the values contained in the array.
Generally it has to be defined, what is meant by declaring an array instance to be const
. In the first place this means constness of the container to the compiler. The compiler will ensure, that the container (array class) is not changed, thus no data member of the array is changed. This means that the array will keep the reference to the same data and that the index-mapping defined by the array shape may not be changed. However, the compiler will not prevent to change any data, the array refers to.
We may define access operators that have a non-const version that returns a reference to the data, allowing to change the data values together with a const version that returns a value of const reference, thus preventing the data from being changed through an instance that is declared const. However, the compiler will always allow to create a non-const copy of a const array instance. In the sense of const-ness of C++ data, this does not violate the const-ness of the source of the copy. The shape of the original array may not be changed. Only the shape of the copy may be changed. But the data of the original array may now be changed through the copied instance, since our array classes implicitly have reference semantic. Thus we have to distinguish between const-ness of the container (array class instance) and the contained data (values in memory the arrays refers to).
In this library we will not provide a const and a non-const version of the array classes. With templated code it is more convenient to use an array with element type const
T
as the const version of an array with element type T
. To allow conversion of an instance with element type T
to an instance of type const
T
, we use the version for elements of type const
T
as a base classe.