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.
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.
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.
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).
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.
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).
virtual
KeywordWhen 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{...}
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)}
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
.
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.
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:
Dim
Must be a type with a static unsigned integer called Value
that specifies the array’s
dimensionality. For example
1struct TenD { 2 static unsigned int Value = 10; 3};
DataType
The type of data to store in the array.
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.