Skip to content

Code Guidelines

ramirezfranciscof edited this page Mar 2, 2019 · 7 revisions

"When a project reaches the point at which no one completely understands the impact that code changes in one area will have on other areas, progress grinds to a halt."

- Steve McConell, Code Complete

General Considerations

Indentation and spacing

  • Always use 3 space indentation. Don't use tabs since (1) these are special characters with special functions (see how makefiles work) and (2) can be interpreted and shown differently depending on text processor configuration.
  • Indent at every change of scope and loop/conditional construct. If you loop through two or more indexes consecutively, you may advance indentation only once for the contained code (as long as there is no commands being executed in between loops). The same applies when checking for different conditions, but try not to do it when mixing loops and conditions.
  • In general, comments should have the same indentation as the section referred to. The commenting symbol should, however, be kept at the start of the line.
  • In general, preprocessor commands should be treated as regular code for indentation purposes. So, for example, an #ifdef section should cause the contained code to have an additional indentation level. Correspondingly, if the #ifdef is inside a loop, it should be indented accordingly (the # symbol remains in the first line).
  • Spacing should be used to improve readability whenever possible: in mathematical and logical expresions separating variables/operators (num1 = num2 * ( num3 + num4 )); in lists separating variables (call subrout( var1, var2, var3 )).
  • Indentation/spacing may be used more liberally when it improves readability: in line continuations, when similar expresions/declarations are being used consecutively.

Naming Conventions

  • Try to use descriptive names, specially for variables/procedures that will be used in different sections.
  • Try not to use single letter variables in long code sections, not even for indeces. Searching for ii instead of simply i makes it much easier to find all instances of looping, for example.
  • Try to consistently use snake_case rather than CamelCase.
  • Try to use all lower case for variables (number = number_matrix(ii,jj)), all upper case for constants (circle_perim = CONST_PI * circle_diam), and consider using verb capitalization for procedures (number = Sum_integers(ii,jj); may be relevant to diferentiate it from an array).

Unit Tests

Others

Fortran Code

  • Use the implicit none statement in all program units: main programs, loose subroutines and module scope. Procedures inside modules will automatically inherit it (exception: procedure specifications defined within interface blocks).
  • Always use the only: statement to specify which variables/procedures of a module you are using. Sometimes it can be useful to rename variables when importing them from a module: use modname, only: new_varname => old_varname.
  • Avoid initializing internal subroutine variables when declaring them because they will have the save attribute and it can get messy with, for example, recursive calls. It is ok to do so for setting default values in data-only modules.
  • Always initialize non-allocatable module variables where they are defined. If these are input keywords, these should be the default value and they should be used in the namelist module.
  • Fortran matrices have a column major order: it stores in memory first the array of the first index, then the second, etc. That means that if you need to sweep through a matrix, it is usually more efficiently to do so having the innermost loops run though the first index.
  • Make your functions pure (and your subroutines whenever possible) and always use the result keyword to specify the output. Making functions pure helps with compiler optimization and specifying result may be relevant in cases such as recursive functions.
  • Always use subroutines for runtime sized outputs. This is because a function will create an unnecessary temporary variable to store the result of the operation. Moreover, some compilers will default to create that variable from stack memory instead of heap memory, which is faster but scarcer. It's easier to just pass the output as a parameter (by reference) and have the calling procedure choose how to deal with that memory. Functions should be reserved for fixed size outputs.
  • Dealing with complex numbers (TBD).
znum = complex( rpart, ipart ) ! will have the same kind as rpart and ipart
rpart = real( znum, kind=8 ) ! will have the kind specified

Module structures

  • Every distinct feature should have its own separated module(s): if your feature needs to affect variables in another feature, it should do so through subroutines stored inside a feature_subs module. If your feature needs to store data/information, it should do so inside a separate feature_data module. Try to balance out minimizing modifications needed in external sections to use the feature and maximizing flexibility in how the feature can be used.
  • Subroutines inside feature_subs should only use data from its own feature_data module. If a feature needs to use data from another module, it should use it through its data module (in which case you may want to rename the variable to make it clear it is not from your module): use feature0_mod, only: ext_var => var in feature_data and use feature_data, only: ext_var in the subroutine.
  • Variables inside feature_data should not be modified directly but via feature_subs subroutines. Be mindful and put some thought into what is the best way and where the best location to set this variables up (using init/setup subroutines, keeping internal logical status variables, etc).
  • Store many-files modules inside a feature_subs folder. There you can have a feature_subs.f90 file that has the module that includes all other relevant files, and a feature_subs.mk that specifies the dependency for all those included files.

Type structures

  • Use customized types to create useful data structures. DISCUSION: How should we use procedures and types? setup(typeobj,data) VS typeobj%setup(data): how do we keep the implementation of the type hidden?
  • Naming conventions: NEW_typename functions for creators, KILL_typename subroutines for destroyers.

Type casting

Type of variable     Value Assigned
integer              INT(expr , KIND = KIND (variable))
real                 REAL(expr , KIND = KIND (variable))
complex              CMPLX(expr , KIND = KIND (variable))

Note that these work as functions that return the corresponding value (Source)

C and CPP Code

Makefiles

Preprocessor and includes

If you #include (ask the preprocessor to include), then preprocessor directives will be processed in the included file. If you include (ask fortran compiler to include), then the included file will not be preprocessed. Don't know if there is an analogous situation with C.

IDEA: the most basic modules should not have preprocessing inside. For example, there could be a separate module for cpu math and gpu math, and the preprocessing that decides which to use is external to those modules.

Sources