Skip to content

Appendix Coding Guidelines

Luke Shingles edited this page Dec 13, 2023 · 1 revision

These guidelines are intended to prevent bugs and increase the readability of the code to other users (and your future self).

Naming functions and variables

A goal is to improve the clarity of function and variable names. In general using longer, more descriptive names is preferred over cryptic short ones.

Functions headers

Functions that return a physical quantity should have the units listed in a comment at the top of the function.

Minimize variable scope

A variable is accessible to some part of the code (the scope) that depends on where it is declared. Minimizing a variable's scope means reducing the number of lines of code that can alter its value, which makes it easier for the programmer (and the compiler) to correctly reason about what the variable contains as the program runs. This is especially important when working on a multithreaded code. Globally-scoped variables (declared inside a header file) are accessible from anywhere in the program, and should be avoided where possible. Variables declared in a source file (outside of any functions) will have file scope, while variables declared inside a C++ block between { and } will exist only within their enclosing block.

For example, the following code:

for (int upperion = 0; upperion < nions; upperion++)
{
  printout("upperion = %d", upperion);
}

int lowerion = 3;
printout("lowerion = %d", upperion);  // syntax error: ion is out of scope here

could be improved by reducing the scope of upperion:

int ion;

for (upperion = 0; upperion < nions; upperion++)
{
  printout("upperion = %d", upperion);
}

int lowerion = 3;
printout("lowerion = %d", upperion);  // logical error but valid syntax

Not only do we eliminate an extra line of code, but the logical error on the last line (using upperion instead of lowerion) is now a syntax error that the compiler will detect and then abort the compilation. With fewer variables available to any line of code, the chance of accidentally using an incorrect one that affects a running simulation is reduced.

Use assertions

Assertions will crash the code at runtime if a certain conditions is not true, which is far better than the simulation continuing to run while silently producing nonsense output. Typically useful conditions are things like an ion/element/cell index being beween zero and the maximum valid index, and physical quantities having a finite value and not being negative. In ARTIS, we define two types of assertions, assert_always() and assert_testmodeonly(). assert_always() should be used for non-performance critical code, such as startup routines, and once-per timestep operations. assert_testmodeonly() can be used anywhere in the code (such as packet propagation) without slowing down real simulations, as they are only checked when the testmode is enabled (such as GitHub actions automated tests).

This kind of error checking is why using getter and setter functions to access local variables (although they clutter the function list) can be better than using a global variable.

Getter and setter functions

Combining the strategies of minimizing variable scope with assertions for error checking, we can mediate global access to file-scope (static) variables using getter and setter functions, as in the example below.

static double ni56_massfraction = -1;  // only this source file can directly access the variable

// the following two functions would declared in the header file, so any part of the code can use them

void set_ni56_massfraction(const double newmassfrac)
{
  assert_always(newmassfrac >= 0);
  assert_always(newmassfrac <= 1.);
  ni56_massfraction = newmassfrac;
}

double get_ni56_massfraction(void)
{
  assert_always(newmassfrac >= 0);
  assert_always(newmassfrac <= 1.);
  return ni56_massfraction;
}

Unlike the alternative of defining a global variable, this code will prevent two kinds of errors: setting the mass fraction to an incorrect value (not between 0 and 1.), and accessing the variable before it has been initialised with a valid value (the initial value of -1 will be detected as an error).