Design conventions¶
Code configuration¶
The code configuration is controlled via the config.h
header file that
is created during the configuration step of the GammaLib build. To make
configuration
options available the following code has to be added to the source code
file:
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
Warning
The config.h file should never be included in header files.
The header files are used by the outside world without any knowledge
about config.h
.
One of the most commonly used configuration options used throughout
GammaLib is related to range checking. Range checks are usually
performed when accessing array elements, assuring that no elements
outside the valid range are accessed. Range checking, however, is time
consuming, in particular if many elements are accessed subsequently.
GammaLib therefore allows to disable range checks. This can be done by
specifying ./configure --disable-range-check
when configuring GammaLib
for compilation. The --disable-range-check
option undefines
G_RANGE_CHECK, and optional range checking is thus achieved by adding
for example
#if defined(G_RANGE_CHECK)
if (inx < 0 || inx >= m_num) {
throw GException::out_of_range("GVector::operator(int)", inx, m_num);
}
#endif
to the code.
The following table gives a list of important configuration options that are available in config.h and that can be used to tune the code:
Definition | Option | Usage |
---|---|---|
G_DEBUG |
--enable-debug |
Code debugging |
G_PROFILE |
--enable-profiling |
Code profiling |
G_RANGE_CHECK |
--enable-range-check |
Performs range checking |
G_NAN_CHECK |
--enable-nan-check |
Check for NaN and Inf values |
G_SMALL_MEMORY |
--enable-small-memory |
Optimizes for small memory |
HAVE_OPENMP |
--enable-openmp |
Has OpenMP multi-threading support |
HAVE_LIBREADLINE |
--with-readline |
Has readline library |
HAVE_LIBCFITSIO |
--with-cfitsio |
Has cfitsio library |
HAVE_PYTHON |
--enable-python-binding |
Has Python bindings |
PACKAGE |
n.a. | gammalib |
PACKAGE_PREFIX |
n.a. | Installation location (e.g. /usr/local/gamma ) |
PACKAGE_STRING |
n.a. | Full name and version of GammaLib |
PACKAGE_VERSION |
n.a. | Version of GammaLib (format: x.y.z ) |
Note that enable
may be replaced by disable
and with
by without
for
switching off an option.
C++ classes¶
Members¶
Class members should be either private or protected, the latter being generally used when a derived class should be able to access base class data.
Members should be prefixed by m_
and should be in lower case. For long
member names, additional underscores may be added. Examples of valid
member names are
m_num
m_response
m_grid_length
m_axis_dir_qual
Initialisation, copying and deleting of class members should be gathered in a single place to avoid memory leaks. For this purpose, each C++ class should have the following private or protected methods for memory management:
init_members()
initializes all member variables and pointers to well defined initial values. The class should be fully operational and consistent with these initial values. All pointers that will hold dynamically allocated memory should be initialised to
NULL
.copy_members(const &A a)
copies all members from an instance
a
into the class.free_members()
frees all memory that has been allocated by the class. Memory pointers should be set to
NULL
after the memory was deleted to signal that no valid memory is associated to the pointer. This allows for checking if memory has been allocated before it is accessed.
(in the above notation, A
is the class name and a
is an instance of the
class).
An example for valid init_members()
, copy_members(const &A a)
and
free_members()
methods is:
void GEbounds::init_members(void)
{
m_num = 0;
m_min = NULL;
m_max = NULL;
return;
}
void GEbounds::copy_members(const GEbounds& ebds)
{
m_num = ebds.m_num;
if (m_num > 0) {
m_min = new GEnergy[m_num];
m_max = new GEnergy[m_num];
for (int i = 0; i < m_num; ++i) {
m_min[i] = ebds.m_min[i];
m_max[i] = ebds.m_max[i];
}
}
return;
}
void GEbounds::free_members(void)
{
if (m_min != NULL) delete [] m_min;
if (m_max != NULL) delete [] m_max;
m_min = NULL;
m_max = NULL;
return;
}
In this example, one may probably want to add a alloc_members()
method
for memory allocation:
void GEbounds::alloc_members(const int& num)
{
if (num > 0) {
m_min = new GEnergy[num];
m_max = new GEnergy[num];
for (int i = 0; i < num; ++i) {
m_min[i] = 0.0;
m_max[i] = 0.0;
}
m_num = num;
}
return;
}
This example illustrates several design conventions:
- Always check if a pointer is not
NULL
before de-allocating memory. - After de-allocation, always set the pointer immediately to
NULL
. - Never allocate zero elements (check if the number of elements to be allocated is positive).
- Always initialise allocated memory to well-defined values (do not expect that the compiler will do this for you).
Constructors, destructors and operators¶
Each class should have at least a void constructor, a copy constructor,
a destructor and an assignment operator. Additional constructors and
operators can be implemented as required. The following example shows
the basic implementation for these 4 methods. Due to the usage of the
init_members()
, copy_members(const &A a)
and the free_members()
methods, most classes will have exactly this kind of syntax:
GEbounds::GEbounds(void)
{
init_members();
return;
}
GEbounds::GEbounds(const GEbounds& ebds)
{
init_members();
copy_members(ebds);
return;
}
GEbounds::~GEbounds(void)
{
free_members();
return;
}
GEbounds& GEbounds::operator= (const GEbounds& ebds)
{
if (this != &ebds) {
free_members();
init_members();
copy_members(ebds);
}
return *this;
}
Inheritance¶
Class inheritance is central feature of the C++ language, and is largely used throughout GammaLib. Multiple inheritance is not used at the moment in GammaLib. Because of the added complexity of multiple inheritance in C++ and in Python there would have to be very good reasons to use it in GammaLib.
Although the inheritance philosophy may differ from class to class, the following guidelines should be respected as far as possible:
The base class and derived class destructors should be declared virtual.
Avoid overloading of base class methods by derived class methods. Preferentially, define base class methods as pure virtual.
All base class methods that should be implemented in the derived class should be declared virtual. Exceptions are the
init_members()
, thecopy_members()
and thefree_members()
methods that will be implemented in the base class and the derived class.Base classes manage base class members, derived classes manage derived class members. By managing we mean here in particular memory allocation and de-allocation, but also proper initialization.
Derived class constructors should invoke base class constructors for proper base class initialization. A void constructor should look like
GEventList::GEventList(void) : GEvents() { init_members(); return; }
and a copy constructor should look like
GEventList::GEventList(const GEventList& list) : GEvents(list) { init_members(); copy_members(list); return; }
Derived class operators should invoke base class operators, as illustrated by the following example:
GEventList& GEventList::operator=(const GEventList& list) { if (this != &list) { this->GEvents::operator=(list); free_members(); init_members(); copy_members(list); } return *this; }
The
clear()
method of a derived class show invoke thefree_members()
method of the base class, as illustrated by the following example:void GCTAEventList::clear(void) { free_members(); this->GEventList::free_members(); this->GEvents::free_members(); this->GEvents::init_members(); this->GEventList::init_members(); init_members(); return; }
Avoid as far as possible methods that are only defined in the derived class.
Warning
For a derived class, init_members(), copy_members(const &A a) and free_members() should only act on derived class members but not on base class members. Any exception to this rule needs very careful documentation since it can easily be the source of memory leaks.
Method naming conventions¶
Uniform public method names should be provided throughout GammaLib for all classes. Unless the public method names are very long (which should be avoided), names should not comprise underscores as separators. Public method names are all lowercase.
Private or protected method names may differ from this since they are hidden within the class. Yet also here, all method names should be lowercase, and the use of underscores should be limited.
Methods that set or retrieve class attributes should be named after the
attribute. Here an example for the attribute m_name
:
public:
void name(const std::string& name);
std::string name(void) const;
protected:
m_name;
A method name that is used in multiple classes should always perform an equivalent action. Here is a list of method names that are widely used in GammaLib, together with their typical usage. The last column specifies where these methods are used. Note that the ``clear()``, ``clone()``, and ``print()`` methods should be implemented for all classes.
Method | Usage | Implementation |
---|---|---|
clear |
Set object to initial empty state | all classes |
clone |
Provides a deep copy of the class | all classes |
print |
Print object into string | all classes (see Output) |
is_empty |
Checks for emptiness of object | Container classes |
append |
Append element to list of elements | Container classes |
extend |
Append container elements to list of elements | Container classes |
insert |
Insert element to list of elements | Container classes |
remove |
Remove element from list of elements | Container classes |
reserve |
Reserve memory for a number of elements | Container classes |
load |
Load data from file (open, read, close) | if applicable |
save |
Save data into file (open, write, close) | if applicable |
open |
Open file | if applicable |
read |
Read data from open file | if applicable |
write |
Write data into open file | if applicable |
close |
Close file | if applicable |
name |
Name of object | if applicable |
type |
Type of object | if applicable |
size |
Size of object | if applicable |
real |
Returns double precision value | if applicable |
integer |
Returns int value |
if applicable |
string |
Returns std::string value |
if applicable |
Note the difference between load()
and read()
and between save()
and
write()
. The load()
and save()
methods should take as arguments a file
name, and they will open the file, read or write some data, and then
close the file. In contrast, read()
and write()
will operate on files
that are already open, and after the read or write operation the files
will remain open. Typically, these methods take a GFits*
or a
GFitsHDU*
pointer as argument.
Methods that perform checks should return a bool type and should start
with the prefix is_
or has_
, e.g. is_empty()
or has_min()
.
Method const declarations¶
All methods that do not alter accessible class members should be
declared const
. With accessible we mean here class members that can be
read or written in some way by one of the methods. Non-accessible class
members would be members that are only used internally, and for which no
consistent state has to be preserved for the outside world. These could
for example be members that hold pre-computed values.
Methods that do not alter accessible members, but that modify
non-accessible members, should also be declared const
. The
non-accessible class members need then to be declared mutable
to avoid
compiler errors. Alternatively, the const_cast
declaration can be used
to allow member modifications within a const
method.
As example we show here part of the definition of GModelSpectralPlawPhotonFlux:
class GModelSpectralPlawPhotonFlux : public GModelSpectral {
public:
virtual double eval(const GEnergy& srcEng) const;
virtual void read(const GXmlElement& xml);
protected:
// Protected members
GModelPar m_integral; //!< Integral flux
GModelPar m_index; //!< Spectral index
GModelPar m_emin; //!< Lower energy limit (MeV)
GModelPar m_emax; //!< Upper energy limit (MeV)
// Cached members used for pre-computations
mutable double m_log_emin; //!< Log(emin)
mutable double m_log_emax; //!< Log(emax)
mutable double m_pow_emin; //!< emin^(index+1)
mutable double m_pow_emax; //!< emax^(index+1)
mutable double m_norm; //!< Power-law normalization (for pivot energy 1 MeV)
mutable double m_g_norm; //!< Power-law normalization gradient
mutable double m_power; //!< Power-law factor
mutable double m_last_integral; //!< Last integral flux
mutable double m_last_index; //!< Last spectral index (MeV)
mutable double m_last_emin; //!< Last lower energy limit (MeV)
mutable double m_last_emax; //!< Last upper energy limit (MeV)
mutable GEnergy m_last_energy; //!< Last source energy
mutable double m_last_value; //!< Last function value
mutable double m_last_g_integral; //!< Last integral flux gradient
mutable double m_last_g_index; //!< Last spectral index gradient
This class has an internal cache for precomputation, which is potentially updated when GModelSpectralPlawPhotonFlux::eval() is called. Here the corresponding code:
double GModelSpectralPlawPhotonFlux::eval(const GEnergy& srcEng) const
{
// Update precomputed values
update(srcEng);
// Compute function value
double value = integral() * m_norm * m_power;
// Return
return value;
}
As the pre-computation cache is not exposed to the external world but
fully handled within the class, eval()
is declared const as it does not
modify any of the model parameters (which are m_integral
, m_index
,
m_emin
, and m_emax
). It may however modified some of the cache
members, that’s why these members are declared mutable
. As there is
however no way to access these cache values from the outside (no method
exists to access them), the eval()
method does not modify any
observable property of the class, hence it is declared const
.
Method arguments and return values¶
If possible, method arguments should always be passed by reference. To
protect references from changes by the method, arguments passed by
reference should always be declared const. Pointers should only be
used as arguments if NULL
should be a possible argument value. Also
pointers should always be declared const
. Here an example based on the
definition of GObservation:
class GObservation {
public:
void events(const GEvents* events);
void statistics(const std::string& statistics);
protected:
std::string m_statistics; //!< Optimizer statistics (default=poisson)
GEvents* m_events; //!< Pointer to event container
};
The statictics
value is passed by reference because the class will hold
the actual value, while events
is passed as a pointer because the class
will hold the pointer.
Numeric argument types should be either int
or double
. Unless absolutely
necessary, avoid short int
, long
, and float
.
If a method returns a class member, the return value should be passed by
reference. Unless we explicitly want to modify a class member through
the method call, return values passed by reference should be declared
const
.
If a method returns a base class object, a pointer should be returned.
Do never return base class objects by reference, as this will lead to
code slicing if the method is used for object assignment. Unless we
explicitly want to modify a class member through the method call, the
returned pointer should be declared const
.
Here an example based on the definition of GObservation:
class GObservation {
public:
virtual double ontime(void) const = 0;
const GEvents* events(void) const;
const std::string& statistics(void) const { return m_statistics; }
protected:
std::string m_statistics; //!< Optimizer statistics (default=poisson)
GEvents* m_events; //!< Pointer to event container
};
The GObservation::ontime() method does return a double
by value as the ontime
property
is not stored explicitly in the class (hence no reference can be
returned to it). On the other hand, the statistics
method returns by
reference as the statistics
property is stored as a data member (hence a
reference can be returned). Although we could have returned a reference
to the event container, this would lead to code slicing. Therefore, the
events
method returns a pointer. All returned references or pointers
are declared const
to prevent modification of class members.
Container classes¶
Container classes are classes that contain list of elements. Two cases are distinguished here: containers holding objects, and containers holding pointers to objects.
Containers holding objects¶
Containers holding objects should have element access operators
operator[]
implemented that return container elements by reference. A
non-const and a const version of the operator should exist. Eventually,
at()
methods could be added that always perform range checking. Here is
a list of mandatory methods for container classes holding objects:
Method | Usage |
---|---|
operator[](const int&) |
Element access operator |
const operator[](const int&) const |
Element access operator (const version) |
void clear() |
Delete all objects in container |
bool is_empty() |
Checks whether container is empty |
int size() |
Return number of elements in container |
void append(const e&) |
Append an element to the container |
void insert(const int&, const e&) |
Insert an element into the container |
void extend(const C&) |
Append another container to the container |
void remove(const int&) |
Removes an element from the container |
void reserve(const int&) |
Reserve memory space in a container |
std::string print() |
Print container (see Output) |
Containers holding pointers¶
Containers holding pointers are different from those holding objects in that
their operator[]
operators return a pointer, and in that they implement
a set()
method for value setting. Here is a list of mandatory methods
for container classes holding pointers:
Method | Usage |
---|---|
e* operator[](const int&) |
Element access operator |
const e* operator[](const int&) const |
Element access operator (const version) |
void set(const int&, const e&) |
Set an element of the container |
void clear() |
Delete all objects in container |
bool is_empty() |
Checks whether container is empty |
int size() |
Return number of elements in container |
void append(const e&) |
Append an element to the container |
void insert(const int&, const e&) |
Insert an element into the container |
void extend(const C&) |
Append another container to the container |
void remove(const int&) |
Removes an element from the container |
std::string print() |
Print container (see Output) |
Output¶
Output stream and logging operators should be implemented for every class as friend operators (see Header file structure). The usage of friend operators (instead of member operators) allows for correct handling of code such as
log << std::endl << "This is a text" << std::endl;
To support these friend operators (and to support also the Python
interface), each class should have a print()
method:
std::string print(const GChatter& chatter = NORMAL) const;
In case that the class derives from one of the standard interface classes
GBase, GContainer and GRegistry,
the output stream and logging operators are automatically implemented on
the level of the base class.
In all other cases, the developer needs to implement these operators on the
class level.
The operators will use the print()
method to enable printing in the
following form:
std::ostream& operator<< (std::ostream& os, const GFits& fits)
{
os << fits.print();
return os;
}
GLog& operator<< (GLog& log, const GFits& fits)
{
log << fits.print(log.chatter());
return log;
}
Exceptions¶
Exceptions are largely used in GammaLib to handle the occurrence of unexpected events. GammaLib exceptions are implemented by the GException class. For each new exception type, a new exception subclass is added.
Each exception returns the method name in which the exception occurs and an exception message. The exception message is generally built from values that are passed as arguments to the exception constructor.
Below a list of conventions for implementing and using exceptions:
- Re-use existing exceptions as far as possible.
- Pass exception arguments by reference.
- Use exceptions only for events that cannot be handled by a method. Do not use exceptions to check a value or a state. Implement appropriate methods instead.
- Never use exceptions in a destructor.
- De-allocate all memory that is not de-allocated by the destructor before throwing an exception.
- Always catch exceptions by reference.
Python interface for C++ classes¶
Container classes¶
The Python interface for container classes should implement the following class extensions:
Extension | Method | Usage |
---|---|---|
__str__ |
print |
Convert object to string |
__getitem__ |
operator[] |
Element access (get) |
__setitem__ |
operator[] |
Element access (set) |
__len__ |
size |
Container size |
While __str__
and __len__
are implemented in the GContainer
base class, __getitem__
and __setitem__
need to be implemented
as class extensions in the SWIG file (see Python interface for C++ classes).