|
minimc 0.5.1
|
This guide is an overview of software design principles that MiniMC developers should adhere to. C++ is intended to be an efficient language but it does not necessarily mean that it has to be complicated. The C++ standard has undergone multiple revisions over the course of decades and, as a consequence, has syntax that supports many ways of doing the same thing. Often, there exists an idiomatic approach that clearly expresses intent without compromising performance. Developers must always be aware of how their code may perform, but never to the detriment of readability. Avoid premature optimization unless you can quantitatively prove its benefits via benchmarking.
Sources for information herein come primarily from the C++ Core Guidelines. This seems to be the definitive guide for writing "modern" C++. Occasionally, matters of opinion refer to Stack Overflow.
Generally speaking, comments should avoid explaining what the code is doing - that is the duty of writing "expressive" code. Rather, comments should largely be dedicated to why. If possible, try justifying certain code so that other developers can understand why you are implementing a feature in a particular way. Ideally, you may even cite the C++ Core Guidelines:
A quick internet search of "C++ Core Guidelines C.131" will show this refers to C.131: Avoid trivial getters and setters.
Unit tests make use of the Catch2 unit testing framework. It is recommended to follow a test driven development process which roughly follows
During testing, it may be useful to break into a debugger to examine why unit tests are failing. For lldb, this can be done by
Use const as much as possible.
Sometimes, a const member variable (such as const std::vector) needs to be initialized using non-const methods (such as std::vector::push_back()). In such cases, one can create a static helper function which will construct a temporary object which is then used to initialize the const member.
In the above code, the constructor for World accepts parameters (...) which are passed onto CreateCells() which returns a properly-initialized temporary object. The temporary object is then used to move construct the const member cells.
Alternatively, one may use the Immediately Invoked Function Expression (IIFE) pattern to perform complex initialization of a complex const member (in this case World::cells).
Both approaches are used throughout MiniMC. IIFE is generally preferred but if it becomes unwieldly and difficult to read, then a static helper function may be warranted.
MiniMC users create XML input files to specify problem geometry, materials, and other settings. MiniMC uses two XML libraries: Xerces-C++ for XML validation and pugixml for object construction. Xerces-C++ was selected since it supports XSD 1.0 validation. For object construction, pugixml was selected since it is overwhelmingly easier for developers to use.
MiniMC expects XML input files to follow a particular format. Malformed input files should be reported back to the user. There are two main places where developers can catch malformed input files: XML validation and object creation.
The input file first undergoes XML validation when it is loaded by XMLDocument. Input files can link to an external schema file in the top-level node:
Ideally, developers will catch input file errors during XML validation. This allows errors to be caught before they can propagate downstream. Downstream code can also be guaranteed that the XML input file validates against a schema so certain checks (such as the existence of particular child nodes for a particular parent node) can be skipped entirely.
In some cases, the schema is not expressive enough to capture input files which are malformed. For instance, the following input file may pass validation but is still incorrect since the cell badcell refers to nonexistent, a surface which does not exist:
In this case, such an error should be caught during the construction of Cell.
Functions may accept variables as parameters and return variables. Some common practices are outlined here.
It is generally recommended to avoid passing by value when one could otherwise pass by const reference. Passing by const reference avoids the overhead of copying a potentially large object:
The first function creates a copy of x while the second function only copies a pointer to x.
One notable exception is when the type itself can be smaller than the pointer used by references. On one system, a reference might be implemented as an 8-byte pointer whereas a char only occupies 1-byte. In this case, passing the char by const reference may actually be slower than passing by value. A general disucssion on when to pass by reference or pass by value is available here.
A function which intends to return a non-mutable object can either return by value or by const reference:
In some cases, returning an existing object by value will return a copy of that object. If the object is large, there may be a significant cost incurred in copying such a large object. This can be addressed by returning a const reference. Care must be taken to ensure that the referenced variable does not go out of scope.
In other cases, returning a large object created in the local scope can avoid (elide) copying by using return value optimization (RVO) when it applies.
This is gonna be a long discussion.