3.4 Core Nektar++ Programming Concepts

This section highlights some of the programming features that are used extensively within Nektar++. While much of the code consists of standard C++ practices, in some of the core infrastructure there are several practices that may be only familiar to programmers who have developed code using more advanced C++ features. Below we give a short summary of these entities in order to provide a starting point when working with these features. We begin with more well known features and end with some advanced techniques. Note, it is not the purpose of the following sections to cover in detail each of these important concepts, but instead to give a brief overview of them such that the developer may look to other, more in-depth, sources if they require further guidance.

3.4.1 Namespaces

Many C++ software projects place their code in a namespace so as to avoid conflicts with other code when included in larger applications. It is important to note that Nektar++ uses a hierarchy of namespaces for most of the defined data structures. The top level namespace is always “Nektar”, with the second level usually corresponding to the name of the library to which the code belongs. For example:

1namespace Nektar 
2{ 
3namespace StdRegions 
4{ 
5    ... 
6} 
7}

With this in mind, when you see something like Nektar::SpatialDomains::..., you can usually assume that the second item (in this case SpatialDomains) is a namespace, and not a class.

Note: To make better use of the 80 character width, generally enforced across the Nektar++ source code, we choose not to indent the contents of namespace blocks.

3.4.2 C++ Standard Template Library (STL)

Nektar++ uses of the C++ STL extensively. This consists of common data structures and algorithms, such as map and vector, as well as many of the extensions once found in the Boost library that have become part of the C++ standard and are now used directly.

One of the most important of these features is the use of Shared Pointers (std::shared_ptr). Most developers are somewhat familiar with “smart pointers” (pointers used to track memory allocation and to automatically deallocate the memory when it is no longer being used) for data blocks that are shared by multiple objects. These smart pointers are used extensively in Nektar++ and one should be familiar with the dynamic_pointer_cast function and the concept of the weak_ptr. Dynamic casting allows for safely converting one type of variable into its base type (or vice versa). For example:

1std::shared_ptr<FilterCheckpoint> sptr = 
2        std::dynamic_pointer_cast<FilterCheckpoint>( m_filters[k] ); 
3if( sptr != nullptr ) 
4{ 
5   // Cast succeeded! 
6}

The advantage of using the dynamic cast, in comparison to the C style cast, is that you can check the return value at run time to verify that the casting was valid. A weak_ptr is a pointer to shared data with the explicit contract that the weak pointer does not own the data (and thus will not be responsible for deallocating it). Weak pointers are used mostly for short-term access to shared data.

Another modern code utility used by Nektar++ to support shared pointers can be seen in Nektar++ classes which inherit from std::enable_shared_from_this. This allows a class member function to return a shared pointer to itself. Specifically, it makes available the function shared_from_this() which returns a shared pointer to the object in the given context.

While C++ shared pointers are a powerful resource, there are a number of intricacies that must be understood and followed when creating classes and using objects that will be managed by them. For those not familiar with the C++11 (or previously Boost) implementation, it is highly recommended that you study them in more detail than presented here.

3.4.3 typedefs

Like most other large codes, Nektar++ uses typedefs to create short names for new variable types. You will see examples of this throughout the code and taking a few minutes to look at the definitions will help make it easier to follow the code. In the following example, we create (and explicitly name) the type ExpansionSharedPtr to make the code that uses this type easier to follow. This is particularly true of nested STL data structures where repeated template declarations would make the code harder to follow. A couple of examples are shown below:

1typedef std::shared_ptr<Expansion> ExpansionSharedPtr; 
2typedef std::shared_ptr<std::vector<std::pair<GeometrySharedPtr, int>>> 
3GeometryLinkSharedPtr;

If you are not familiar with the use of typedefs, you should take time to read about them (there are many short summaries available on the web).

3.4.4 Forward Declarations

There are two ways that an existing class type can be specified when declaring a new class in a header file. The existing class can either be declared in name only, or declared in its entirety, before being used. In the latter case, one typically includes the header file declaring the full class. If the new class declaration only references the existing class in the form of a pointer or reference then the entire class declaration is not needed and the compiler only needs an assurance that the class exists. For this case, we can use a forward declaration which tells the compiler the name of the existing class. However, if functions of the existing class are called (within the new header file) or the class is used by value, then the full declaration is needed.

Forward declaring a class is achieved as shown in the following example:

1class LinearSystem;

This statement tells the compiler the class LinearSystem exists and, as long as we only make reference to it as a pointer (LinearSystem* l) or by reference (const LinearSystem & l), then the compiler does not require any further information.

An advantage to using forward declarations where possible is that the header file does not need to #include the entire existing class and any header files referenced within. This allows for a cleaner header files and faster compilation as the compiler can process (often significantly) fewer lines of code.

Note: The full class declaration is most likely needed in the new class implementation file (.cpp) as reference to the existing class’s members will presumably be made.

3.4.5 Templated Classes and Specialization

Most C++ developers are familiar with basic class templating. However, many have not needed to use explicit template specialization. This is the process of implementing customised behaviour for one or more of the specific instances of a template when the compiler will not be able to instantiate a generic version for the class, or when different code is needed based on different versions of the class. For example:

1template<typename Dim, typename DataType> 
2class Array; 
3 
4template<typename DataType> 
5class Array<OneD, const DataType> { 
6  // Explicit coding of class methods and variables specific to 
7  // this version of Array are found here. 
8};

In the above example, on the first line the generic templated Array class is declared. There are two template parameters: the dimension and the element type. The second line shows an explicit template specialisation of the Array class for a one-dimensional (version of) Array. When explicitly specialising a class, the programmer will write code that is specific to the datatype used in specifying the class. This includes explicitly writing code for one, some, or all of the methods of the class.

It is important to understand template specialization when dealing with the Nektar++ core libraries so that the developer can determine which (specialized version of the) class is being used, and to know that when updating classes with varied specializations, that it may be required to update code in several places (ie, for each of the specializations).

3.4.6 Multiple Inheritance and the virtual Keyword

When diving into many Nektar++ classes, you will see the use of multiple inheritance (where a class inherits from more than one parent class). When the parent class does not inherit from other classes, then the inheritance is straightforward and should not cause any confusion. However, when a class has grandparents, many times that grandparent class is the same class but is inherited through multiple parents. To account for this, class inheritance should use the virtual keyword. This specifies that if a class has multiple grandparents (that happen to be the same class), that only one copy of the grandparent class members should actually be instantiated. For example:

1class Expansion2D : virtual public Expansion, 
2                    virtual public StdRegions::StdExpansion2D 
3{...}

3.4.7 Virtual Functions and Inheritance

Within Nektar++, classes that inherit from a parent class and override one of the parent class methods, use the concept of virtual functions. The function is prefixed with a v_, such as v_Function(), as a naming convention. This is a visual reminder that the function overrides a parent class function. For example:

1NekDouble TriExp::v_Integral(const Array<OneD, const NekDouble> &inarray)}

3.4.8 Const keyword

While the const keyword is known to most C++ developers, it is used (as it should be) liberally in Nektar++ for functions, function parameters, returning pointers to class data, and variable constants within functions. It is easy to neglect using const to mark all cases where a variable should be considered constant. However, its use can substantially reduce accidental errors and allow for accelerated debugging. The const qualifier should be used wherever a variable does not change including 1) parameters passed to functions, 2) variables in functions (or classes) that do not change value during their lifetime, 3) on the return type of functions that return pointers to data that should not be changed, and 4) on methods that do not change data within the class. The compiler will then produce an error if we (accidentally) attempt to make a change which violates a const.

3.4.9 Function pointers and bind

Function pointers (std::function) are similar to pointers to data, except that they point to functions - and thus allow a function to be invoked indirectly (in other words, without explicitly writing the function call (name) directly in code). This technique is used by Nektar++ in a number of places, with NekManager being a prime example. The NekManager class is used to create objects of a specific type during the execution of the program. When a NekManager is created (constructed), it is provided with a pointer to a function that will (later) be called to generate the objects to be managed when required. While the creation function that is provided to the NekManager takes a number of parameters, in many cases some of the values to those parameters will be fixed. To handle this situation, Nektar++ uses the std::bind( f ) function, which creates a new function based on supplied original function f, but specifies that one or more parameters of f are fixed at the time that f is created and only those bound parameter values will be used when f is later invoked.

3.4.10 Memory Pools and NekArray

An Array is a thin wrapper around native arrays. Arrays provide all the functionality of native arrays, with the additional benefits of automatic use of the Nektar++ memory pool, automatic memory allocation and deallocation, bounds checking in debug mode, and easier to use multi-dimensional arrays.

Arrays are templated to allow compile-time customization of its dimensionality and data type.

Parameters:

It is often useful to create a class member Array that is shared with users of the object without letting the users modify the array. To allow this behavior, Array<Dim, DataType> inherits from Array<Dim, const DataType>. The following example shows what is possible using this approach:

1class Sample { 
2public: 
3    Array<OneD, const double>& getData() const { return m_data; } 
4    void getData(Array<OneD, const double>& out) const { out = m_data; } 
5 
6private: 
7    Array<OneD, double> m_data; 
8};

In this example, each instance of Sample contains an array. The getData method gives the user access to the array values, but does not allow modification of those values.