minimc 0.5.1
Loading...
Searching...
No Matches
Developer Guide

Introduction

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.

Commenting

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:

class CSGSurface {
public:
// Unique user-defined identifier (C++ Core Guidelines C.131)
const std::string name
}
A surface to be used in constructive solid geometry.
Definition: CSGSurface.hpp:15
const std::string name
Unique, user-defined identifier (C++ Core Guidelines C.131)
Definition: CSGSurface.hpp:39

A quick internet search of "C++ Core Guidelines C.131" will show this refers to C.131: Avoid trivial getters and setters.

Workflow

Test Driven Development

Unit tests make use of the Catch2 unit testing framework. It is recommended to follow a test driven development process which roughly follows

  1. Define feature requirements in terms of a Catch2 unit test
  2. Write unit test which only test public interfaces. Do not test private members. They should be indirectly tested by producing correct results in the public interfaces.
  3. Write source code to implement the feature until the unit test works. Do not worry too much about optimization.
  4. Refactor for clarity and optimizations if necessary.
  5. Repeat.

Debugging

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

lldb all_tests --one-line "break set -E c++" -- --break

Const

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.

// Note: The following code won't compile as written because of the declaration
// order but this will not be an issue if split into header and implemenation
// files.
class World {
public:
World(...): cells{CreateCells(...)} {};
private:
static std::vector<Cell> CreateCells(...);
const std::vector<Cell> cells;
}
Represents the state of a nuclear system.
Definition: World.hpp:27
const std::vector< Cell > cells
Cells that appear in the World (C++ Core Guidelines C.131)
Definition: World.hpp:83

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).

class World {
public:
World(...): cells{[](...){
// construct non-const temporary object (usually named `result`)
std::vector<Cell> result;
// populate `result`
result.push_back(...);
// (named) return value optimization guarantees that `result` is not copied
return result;}()
}{};
private:
const std::vector<Cell> 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.

Input Parsing

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.

XML Validation

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:

<minimc
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="minimc.xsd">
...
</minimc>

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.

Object Construction

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:

<surfaces>
<sphere name="exists" x="0." y="0." z="0." r="1."/>
</surfaces>
<cells>
<cell name="badcell">
<surface name="nonexistent" sense="-1"/>
</cell>
</cells>

In this case, such an error should be caught during the construction of Cell.

Variables

Functions may accept variables as parameters and return variables. Some common practices are outlined here.

Passing Parameters

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:

void func(HugeType x); // slow!
void func(const HugeType& x); // fast!

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.

Returning Values

A function which intends to return a non-mutable object can either return by value or by const reference:

HugeType func(); // return by value
const HugeType& func(); // return 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.

Smart Pointers

This is gonna be a long discussion.