NOTE: please carefully evaluate and give feedback.
Page Content
Project structure
The CORSIKA8 structure is set up following the guidance of other large software projects:
- boost, https://github.com/boostorg --> very similar project structure
- ranges-v3, https://github.com/ericniebler/range-v3/tree/master/include/range/v3 --> very similar project structure
- NVIDIA/thrust: https://github.com/NVIDIA/thrust --> the use of
*.inl
files - EvtGen, https://phab.hepforge.org/source/evtgen/browse/master --> similar class (etc) naming conventions
Advantages of this structure are:
- clear correspondence and traceability of files/locations, in particular if they are
#include
d - extremely clear separation between interface definition and documentation on one side, and implementation details on the other side
- identical structure in
source
andbuild/install
environment - very easy to deploy
- very easy to manage with a well designed
FindCCORSIK8.cmake
tool.
Everything is relative to the (cmake) PROJECT_SOURCE_DIR source directory:
-
corsika
all header-only/ public headers - framework code -
modules
all extra packages that needs to get compiles and then interfaced to the framework -
src
the parts of the framework that needs building -
examples
for all examples -
test
for all unit tests, sorted by components -
cmake
cmake resources, build system -
externals
needed crucial external packages -
documentation
for Doxygen resources and configurations etc. -
tools
extra tools and scripts
More details:
the /corsika section is the header-only entry point for C8, and /corsika/details contains all corresponding inline files.
There is one main namespace: corsika
, all aspects of corsika8 are available there. Modules may create their own sub-namespaces if needed.
A process/module should contain code that even a typical bachelor student can comprehend and extend. A master student must be able to setup a new process from scratch, and do his research with this.
Coding guidelines for CORSIKA 8
- We use clang-format for basic code layout. Feel free to open discussions about its settings.
- We predominantly write code in C++17. We will move to C++20, when it is more established and supported.
- We write code motivated along well-known guidelines, please look at those:
Important rules, and notable differences are summarized here:
For components coded in C++
Filenames, File Content and Layout
- c++ header end on
.hpp
like inInteraction.hpp
. Headers files (*.hpp
) should contain Doxygen documentation, macros, as well as class and function declarations. - c++ files end on
.cpp
like inInteraction.cpp
. These files are the entry points for building executables and libraries. - c++ inline files end in
.inl
like inInteraction.inl
. Inline files (*.inl
) should be post- included, outside the concerningnamespace
in the header files with same prefix-name and provide the implementations for the classes and functions declared in the corresponding header. No Doxygen documentation should be stored in*.inl
files. - All files that are meant for inclusion should be guarded with
#pragma once
in order to avoid multiple inclusion in the compilation units. - Header and Inline files are always included with
<path/to/file.hpp>
and not "path/to/file.hpp". - Classes and files containing classes must have the same name. For example, the
class Doer
should be fully declared and documented in the header "Doer.hpp" and have their methods implemented in the file "Doer.inl".
Class, Namespace, Enum, Function, and Variable Naming Conventions
- Free functions are small letters only like
make_bar(Wine const& wine, Beer const& beer)
- Classes are named in upper camel case
class LikeThis
- Class methods are lower camel case
LikeThis::doSomething()
- Data members are lower camel case with a "_" suffix.
LikeThis::parameterOne_
- Namespaces are small letters only like
namespace this_namespace
- Enums follow class rules, e.g.
enum class ThisIsAnEnum {one,two, three};
- Define and use enums as locally as possible, as strongly namespaced as feasible (best inside their host class)
- Do not use abbreviations, if not absolutely necessary
-
utility
notutl
- do not use e.g.
i
, ifi
is used in more than a single line of code; rather useindex
, orcount
or whatever is appropriate - Same for
p
, if it should beparticle
etc.
-
Class Design
-
Classes are designed like this:
- No public data member is allowed. In specific, limited, and obvious cases
struct
may be useful. - Everything that does not need to be visible to the outside of a class/struct should be
private
orprotected
. In particular member data MUST be non-public, but also consider each method, even constructors etc. - Everything that should not change should be
const
- Access data members using getters and setters
- for setters use either by value
setValue(type value)
or, iftype
is not an integral type by referencesetValue(const type& value)
- for getters use by value
type getValue() const {return value_;}
, or by referencetype& value() {return value_;}
respectivelyconst type& value() const {return value_;}
. Note, use the fromvalue()
only when it returns a reference to a data member, i.e. for function chaining.
- for setters use either by value
- For logical states, use
bool isTrue() const
orbool hasThis() const
. Avoidbool isNotNeeded() const
since it is just!isNeeded()
. - Data member declarations follow after method declarations.
- Classes need to be documented with doxygen commands.
These simple rules are summarized in the snippet below:
// forward declarations class Beers; class Wine; /* \class Bar * \brief Just a very limited bar... */ class Bar { ///! trivial constructor Bar() = default; ///! non-trivial constructor Bar( Beer const& beer, Wine const& wine): beer_(beer), wine_(wine) {} ///! move constructor Bar( Bar && other) = delete; ///! copy constructor Bar( Bar const& other): beer_(other.getBeer()), wine_(other.getWine()) {} ///! assignment operator Bar& operator=( Bar const& other){ if(this=&other) return *this; beer_ = other.getBeer(); wine_ = other.getWine(); return *this; } void DrinkAll(void); Beer getBeer() const { return beer_; } void setBeer(Beer const& other){ beer_=other; } // for function chaining: Bar& beer(Beer const& other){ beer_=other; return *this; } Wine getWine(){ return wine_; } // for function chaining: Wine& wine(){ return wine_; } void setWine(Wine const& other){ wine_=other; } // data members last private: Beer beer_; Wine wine_; };
- No public data member is allowed. In specific, limited, and obvious cases
-
Reflect about the resources the class is holding and if makes sense to move or copy them. Implement non-default or long components in the *.inl.
-
Follow the
rule of zero/five
. This concerns any of the following methods handling an object lifecyle:- default constructor: ```X()``` - copy constructor: ```X(const X&)``` - assignment operator: ```operator=(const X&)``` - move constructor: ```X(X&&)``` - move assignment operator: ```operator=(X&&)``` - this is special, only if any memory allocation needs to be cleaned up, etc, a destructor ```~X()```
Technical references: https://en.cppreference.com/w/cpp/language/rule_of_three
Either you implement any of them (zero), or all of them (five); and if memory/resources must be released also the destructor. Instead of implementing, you may use the defaults
= default
or= delete
keywords to indicate desired behavior. -
Each functional group of classes has a unit test
testThis.cpp
-
Adhere to C++17 standards. In particular:
-
Define public, private and protected type aliases and typedefs on the top, inside the class or method they are necessary. It should be written in minor letters, suffixed by "_type", like in
typedef ClassA someclass_type
. -
Template, meta programming:
- Template (meta) parameters are identified by a prefixed "T", like
template <typename TProperty>
. This makes it easier in writing code to distinguish classes and types from template (meta) parameters.
- Template (meta) parameters are identified by a prefixed "T", like
-
For class templates, provide deduction guides, if applicable. Also, add instance makers as free functions in the concerning namespace, to deploy type deduction and avoid explicit template instantiation. Look the example below:
// prototype example of a class to manage process queue, that should be able to // dispatch processes in parallel o sequentially, according to TParallelPolicy // this simple design allow to compose tasks in groups with different policies template<typename ParallelPolicy, typename ProcessList, typename StackIterator> class ProcessQueue; // specialization and concrete implementation template<template< typename ...T > ParallelPolicy, typename StackIterator, typename ...Processes> class ProcessQueue<ParallelPolicy<T...>, std::tuple<Processes...>, StackIterator >: public ProcessDispacher<<ParallelPolicy<T...>> { public: typedef ParallelPolicy<T...> dispatch_policy; typedef ProcessDispacher<<ParallelPolicy<T...>> dispatcher_type; typedef std::tuple<Processes...> process_tuple_type; typedef std::pair<StackIterator> stack_range_type; // constructor (0) ProcessQueue() = delete; // constructor (1) ProcessQueue(dispatch_policy const& policy, process_tuple_type const& processes, stack_range_type const& range ): TProcessDispacher<<TParallelPolicy<T...>>(policy), processes_(processes), stack_range_() {} // constructor (2) template<typename Stack, typename = typename std::enable_if<detail::is_stack<Stack>>::type > ProcessQueue(dispatch_policy const& policy, process_tuple_type const& processes, Stack const& stack ): TProcessDispacher<<TParallelPolicy<T...>>(policy), processes_(processes), stack_range_(std::make_pair(std::forward<TStack>().begin(), std::forward<TStack>().end())) {} /* . . other methods, ctors, etc . */ void operator()(){ return dispatcher_type::callEach(stack_range_, processes_ ); } private: processes_tuple_type processes_; stack_range_type stack_range_; }; // deduction guide template<typename TParallelPolicy, typename TStack, typename ...TProcesses> ProcessQueue( dispatch_policy const& policy, process_tuple_type const& processes, Stack const& stack)-> ProcessQueue<TParallelPolicy, std::tuple<TProcesses...>, declatype(std::declval<TStack>().begin()) >; // instance maker template<typename TParallelPolicy, typename TStack, typename ...TProcesses> auto make_process_queue(TParallelPolicy const& policy, TStack&& stack, TProcesses const&...processes ) -> ProcessQueue<TParallelPolicy, std::tuple<TProcesses...>, declatype(std::declval<TStack>().begin()) > { return ProcessQueue( policy, std::make_tuple(processes...), std::make_pair(std::forward<TStack>().begin(), std::forward<TStack>().end()) ); }
Technical references:
-
General Rules
- Test your code with
-pedantic
compiler flag enabled. - Warnings have to be fixed, when they appear on the system(s) currently used by the CI, by altering the code unless the warning originates from third-party code and cannot be fixed. In such cases, developers of the third-party project should be informed. Should the warning be locally silenced, either using appropriate pragmas or locally turning off warnings for that translation unit, documentation should be added in the entry point triggering the
warning using the flag
"FIXME"
. - All unit tests must succeed on the specified systems/configurations on gitlab-ci. If tests fail, code is not merged. In certain cases, core developing team will consider to update or extend the CI systems.
- The unit test code coverage should never decrease due to a new merge request.
- Exceptions and error handling
- On major, but rare, errors or malfunction we throw an exception. This is needed and required for complex physics and shower debugging.
- We don't catch exceptions for error handling, there might be very few special exceptions from this. We need to discuss such cases. We want to use exceptions as well defined entry points into e.g. gdb
- References, Pointers
- We prefer the use of references, wherever useful
- There cannot be any raw pointer in the interface of any class or object exposed to outside users, there might be (technical) raw pointers for very special (exceptional) cases inside of classes.
- Consider using standard library smart pointers (
std::unique_ptr
,std::shared_ptr
).
- When you contribute new code, or extend existing code, at the same time provide unit-tests for all functionality.
- When you contribute new physics algorithms, in addition you also need to provide a validation module (this is still TBD what exactly this means)
Comments and Documentation
- Declared code must be documented with
doxygen
commands extensively within the public header files. - There should not be any useless comments in code, in particular absolutely avoid to commit commented-out debug remnants
thisIs = used_code; // thisWas(used_for_debug_only); // <--- This line has to be removed
- Add sufficient and meaningful comments to the code (only in English), but only if the code is not self-explanatory. Better write your code, so that it is clear what is done, for example:
and notTimeType TimeOfIntersection(Line const& line, Plane const& plane) { auto const line_direction = line.GetDirection(); auto const plane_normal = plane.GetNormal(); return plane_normal.dot(plane.GetCenter()-line.GetPosition()) / plane_normal.dot(line_direction); }
TimeType TimeOfIntersection(Line const& l, Plane const& p) { auto const d = p.GetCenter() - l.GetR0(); auto const v = l.GetV0(); auto const n = p.GetNormal(); auto const c = n.dot(v); return n.dot(d) / c; }
For components not coded in C++
- fortran files end on ".f"
sibyll23c.f
- python files end with ".py"
- C header files ends with ".h" and source files with ".c"
CMAKE formatting
- command are lower cases, e.g.
set (...)
- variables upper case, e.g.
set (VAR1 Text)
Since cmake itself lacks structure almost entirely:
- put a space between command and start of parenthesis, e.g.
command (...)
- add two spaces for logical indent
if (condition) do something endif (condition)
- break long lines to start with new keyword in new line (indented)
install ( FILES ${CORSIKAstackinterface_HEADERS} DESTINATION include/${CORSIKAstackinterface_NAMESPACE} )
- add plenty of comments to all cmake code
- use expressive variables and functions