Lattice Builder Manual
Software Package for Constructing Rank-1 Lattices
|
In Lattice Builder, there are three different types of declarations of function arguments:
Although the first prototype allows f
to make a copy of x
internally, it is best to leave it to the compiler to do it. For example, the following code will cause two instantiations of objects of type T:
whereas the following allows the compiler to instantiate only one object of type T:
In multiple situations, Lattice Builder needs to access arrays of values that can interchangeably be computed on-the-fly or pre-computed. The concept of sequence, in the sense of ordered list rather than infinite mathematical sequence, provides in Lattice Builder a unified interface for both. The elements of a sequence are accessed via immutable iterators (const_iterator
) with the standard syntax as used for the standard C++ containers, as in the following example:
Or, using the new C++11 auto
keyword and for
syntax:
Concrete examples are given in the tutorial in sections Sequences of Generator Values, Sequences of Lattice Definitions and Sequences of Merit Values .
The values in a sequence often need to be mapped to other values, possibly of a different type. For that purpose, Lattice Builder introduces the concept of a bridge sequence, which contains the mapped elements in the same order as the sequence it is based on.
Traditional object-oriented programming makes use of dynamic polymorphism, where the exact class of an object is resolved at execution time, dynamically. It follows that polymorphic member function calls cannot be inlined, so it is often not advisable to place such a call in the core of a loop with many iterations, at the risk of cumulating inderections due to both polymorphism and function calls.
Some algorithms implemented by Lattice Builder come in several variants which require either polymorphic function calls inside such loops or a large amount of duplicated code with tiny differences. C++ allows to circumvent that problem through the use of static polymorphism, which enables compile-time resolution of the types together with member function inlining.
In some places, static polymorphism is used regardless that dynamic polymorphism could have been used without any notable performance loss, in order to maintain a consistent design. We prefer making common usage patterns of Lattice Builder's classes easier to recognize, even if that means a little bit more verbose code in places.
The most obvious example is the case of sequence classes. Their member functions begin()
and end()
return iterators; the type of an iterator is often specific to the type of the sequence it is pointing to.
Another example is the size parameter is represented by a different class depending on whether ordinary or embedded lattices are considered. In both cases, the class implements a numPoints()
member function, that returns the number of points in the lattice, and that is accessed frequently in some parts of the code. In a traditional object-oriented approach, the two classes would derive from the same abstract base class, and would have required runtime object-type resolution at the call points of numPoints()
. With the static polymorphism approach, the numPoints()
function can be efficiently inlined by the compiler, thus avoiding both the overhead due to the function call and to runtime object-type resolution.
Concepts in C++ can be thought of as the static-polymorphism counterpart of dynamic-polymorphism interfaces. Like interfaces, concepts can specify requirements on the members of a class; unlike interfaces, the data types of the arguments and of the return value of a member function can differ across classes implementing the same concept. Furthermore, a concept can require type definitions (typedefs or nested classes) inside of a class. A well-known example of concept it that of iterator
or of const_iterator
from the STL.
Although requirements on the types are not explicited with concepts definitions in Lattice Builder, but the code is built around the idiom of concepts.
By convention in Lattice Builder, classes representing the same concept are regrouped in the same namespace.
The most important concepts are described by means of example in the Low-Level API Tutorial.
In Lattice Builder, when a non-trivial object is no longer needed in its scope after it is passed as an argument to a constructor or factory function, it is generally std::move()'d to the constructor or function in question. The compiler takes care of using the default move constructor on the object that is passed to the constructor and thus avoids copying the object, which can safely be stored as a class member of the `owner' object. Generator sequences (see the GenSeq namespace) are an example of non-trivial objects that are transferred to other ̀owner' objects using this mechanism. Weights objects, such as instances of LatCommon::ProductWeights or of LatCommon::ProjectionDependentWeights, are other examples of non-trivial objects, but because they are polymorphic, they are stored using a std::unique_ptr, which in turn, is passed using either std::move() or, equivalently, std::unique_ptr::release(). There are a few exceptions to that ownership policy, notably figures of merit like instances of WeightedFigureOfMerit or of CoordUniformFigureOfMerit. Some classes store references to such objects, so the objects themselves are also stored using std::unique_ptr so that references remain valid even after the object ownership has changed.
Lattice Builder makes uses of the following C++11 features:
The Lattice Builder library depends on the following Boost libraries:
In addition to these, the Lattice Builder command-line tool also depends on the following Boost libraries:
Only the Program Options library requires building and linking; the others are header-only libraries.
The fast CBC implementation of Lattice Builder, as well as the computation of the \(R_\alpha\) criterion, depends on the FFTW library.