Coding standards¶
These rules are enforced by .clang-format, .clang-tidy and code review.
Language¶
- C++20, no compiler extensions (
CMAKE_CXX_EXTENSIONS OFF). - Prefer the standard library. Use
std::format,std::string_view,std::optional,std::spanand ranges where they make code clearer. - No exceptions for control flow. Encoders and parsers return
std::optionalor a result type; exceptions are reserved for programming errors and unrecoverable I/O failures. - No raw
new/delete. Qt objects with a parent are the one exception, since Qt owns them.
Naming¶
| Element | Style | Example |
|---|---|---|
| Namespaces | lower_case |
nmeasim::core::nmea0183 |
| Types | CamelCase |
SerialPortDescriptor |
| Functions and methods | lower_case |
append_checksum |
| Variables and parameters | lower_case |
distance_m |
| Private and protected members | trailing underscore | port_name_ |
Constants and constexpr |
k prefix, CamelCase |
kMaxSentenceLength |
| Files | lower_case with .hpp/.cpp |
serial_ports.hpp |
Units are part of the name: speed_kn, depth_m, bearing_deg, period_ms.
Layout¶
clang-formatwith the repository configuration; 4-space indent, 100-column limit.- One class per header where practical. Headers use
#pragma once. - Include order: own header, project headers, Qt, third party, standard library.
- Every file carries documentation comments as described under Documentation comments.
Qt usage¶
src/corenever includes Qt.- Use
QStringLiteralfor literals,QString::fromStdStringat the boundary withcore. - Prefer the functor-based
connectoverloads over string-based signals and slots. - GUI code never blocks the event loop; long work goes to the engine thread.
Tests¶
- Every public function in
corehas a unit test. - Test files mirror the source path:
src/core/src/geo/geodesic.cppis tested bytests/core/geo/geodesic_test.cpp. - Use Catch2 tags:
[nmea0183],[geo],[io],[integration]. -
The Python tools in
tools/are tested with the standard library'sunittestintools/tests/, onetest_<subject>.pyfile per subject. CI runs them on every platform, after the stream cross-check that installs their dependencies:pip install pynmea2==1.19.0 pyais==2.9.4 python3 -m unittest discover -s tools/tests -v
Documentation comments¶
Every source file in the repository is documented: C++ headers and implementation files, tests, Python tools, CMake files and workflows. The C++ reference on this site is generated from the C++ comments, as ADR 0017 describes, so a comment is written for a reader who sees it next to the signature but not next to the code. The checks listed under Enforcement run in CI.
Syntax¶
- Documentation comments are
///lines in front of the entity they describe. A trailing///<is allowed after a data member or an enumerator when the whole comment fits on that line. - Commands start with
@(@param, never\param). - The first sentence is the brief description: Doxygen ends it at the first full stop
followed by a space. It must therefore contain no abbreviation such as "e.g." or "i.e.";
write "for example" and "that is". Details follow after an empty
///line. - Comments are Markdown: backticks around code, parameter names, file names and literal
values,
-for lists. No HTML. - British spelling, complete sentences, present tense. Functions are described in the third person: "Returns the checksum", not "Return the checksum".
- Lines stay within 100 columns.
clang-formatreflows comments, so reread a comment after formatting. - Name another entity by its name, qualified where it is ambiguous; the reference turns the name into a link.
Files¶
Every C++ file, including the .in templates that CMake configures, starts with the licence
identifier and a file comment whose first sentence says what the file provides:
// SPDX-License-Identifier: GPL-3.0-only
/// @file
/// Geodesic distance, bearing and destination on the WGS 84 ellipsoid.
///
/// Optional paragraphs: the main entry points, the standard the file implements.
#pragma once
The licence line is a plain // comment so that it never becomes part of the
documentation.
Namespaces¶
- Every named namespace is documented exactly once: in the header that declares its central
type or is named after it, with a
///comment in front of thenamespaceline. The enclosing namespaces that have no such header (nmeasim,nmeasim::core,nmeasim::io,nmeasim::app) are documented insrc/namespaces.dox. - The comment says what the namespace contains and, for a library namespace, which layer it belongs to.
- Anonymous namespaces are not documented themselves; every entity inside one is.
Types¶
A class, struct, union, enumeration or type alias states:
- what it represents or is responsible for (the brief);
- its invariants: which combinations of member values are valid;
- ownership and lifetime: what it owns, and the objects it refers to that must outlive it;
- copy and move behaviour when it is not the obvious one, and why copying is disabled when it is;
- thread safety and, for a
QObject, thread affinity, whenever they differ from the defaults below; - the governing standard when it models a protocol structure (see Standards);
- related types with
@see.
Every template parameter has a @tparam.
Functions¶
A function, constructor, operator or method has:
- a brief saying what it does or returns;
@paramfor every parameter: its meaning, its unit when the name has no unit suffix, its valid range and what special values (null, empty,std::nullopt, zero) mean. Output and in-out parameters are marked@param[out]and@param[in,out];@returnfor every non-void function: what is returned, including what an empty or failure value means. Constructors andvoidfunctions have none;@throwsfor every exception type the function throws or deliberately lets propagate, with the condition. Anoexceptfunction has none;@prefor every condition the caller must ensure and whose violation is undefined behaviour or an assertion, and@postfor guarantees about the object's state after the call that@returndoes not already state;@note,@warningand@seewhere they help: a surprising side effect, a performance cost, a related function.
Further rules:
- A function that emits Qt signals names them: "Emits
state_changed." - An override of a function documented in this repository inherits its documentation and
is commented only where its behaviour is more specific. An override of a Qt or standard
library function (
paintEvent,sizeHint,what) is always documented, because its base is not part of this reference: the comment says what this override does. - A defaulted special member gets a one-line comment ("Destroys the transport, closing it first."); a deleted one says why it is deleted.
- Documentation lives at the declaration. A definition in a
.cppfile of a function declared in a header carries no///comment of its own, only//implementation comments; functions local to a.cppfile are documented where they are defined.
Data members, enumerators and constants¶
Every data member (public, protected and private), enumerator and named constant states its meaning, its unit when the name has none, its valid range and what its default or sentinel values mean. An enumerator that maps to a protocol field names the value it is sent as. A constant says where its value comes from: a standard, a measurement, a user-interface choice.
Qt signals and slots¶
- A signal is described as an event: "Emitted when ...". It states the condition, the
meaning of every argument with
@param, whether it is also emitted when the value did not change, and whether it is emitted synchronously from inside a public call. - A slot is documented like any other function and additionally says what normally connects to it.
Macros¶
Every macro states its purpose, documents each argument with @param and says what it
expands to when that matters to the caller.
Units, ranges and defaults¶
Units follow the naming rules above (speed_kn, depth_m, bearing_deg). Comments add
what the name cannot carry:
- angles are degrees true unless marked magnetic; bearings and headings are in [0, 360), latitudes in [-90, 90] positive north, longitudes in [-180, 180] positive east;
- signed quantities say which sign means what (positive to starboard, positive rising);
- times are UTC; durations use
std::chronotypes; - ranges are written in interval notation, and the comment says what happens outside the range: clamped, rejected with an empty result, or undefined behaviour;
- a default value is documented by its initialiser in the declaration; the comment explains why that value was chosen when it is not self-evident.
Ownership, lifetime and threads¶
- Raw pointers and references are non-owning. A function or type that keeps one says who owns the object and how long it must live.
- A
std::unique_ptrparameter transfers ownership; a Qtparentparameter makes the parent the owner, and the comment says so when the argument may be null. - A function returning a reference or a view says how long it stays valid.
- Default thread safety, which comments do not repeat: an object is used by one thread at a
time; distinct objects may be used from different threads;
constmember functions of core types may run concurrently. AQObjectlives in the thread that created it and is used only from that thread. Anything else (a class moved to a worker thread, a signal emitted from another thread, a function safe to call from any thread) is stated.
Standards¶
Code that implements an external specification cites it at the type or function concerned, naming the edition where the code follows a particular one, and the section, message or sentence where one applies:
| Area | Specification | Example citation |
|---|---|---|
| NMEA 0183 sentences | NMEA 0183 (IEC 61162-1) | @see NMEA 0183, sentence GGA. |
| AIS messages | ITU-R M.1371-5 | @see ITU-R M.1371-5, Annex 8, message 5. |
| TAG blocks | IEC 61162-450 | @see IEC 61162-450, TAG block parameter "c". |
| Signal K | Signal K specification 1.7.0 | @see https://signalk.org/specification/1.7.0/doc/data_model.html |
| Geodesics | WGS 84; C. F. F. Karney, Algorithms for geodesics, 2013 | @see GeographicLib::Geodesic |
| Tracks | GPX 1.1, OGC KML 2.2 | @see GPX 1.1, element trkpt. |
Standards that are not freely available are cited by document and clause without a link. Open specifications are linked.
Implementation comments¶
//comments inside functions and on implementation details explain why: the reason for a choice, a constraint that is not visible in the code, the source of a constant, a consequence that is easy to miss. They do not narrate what the next line does.- A comment that contradicts the code is a bug; change both together.
- No commented-out code. Delete it; the history keeps it.
- A
TODOnames its issue:// TODO(#123): .... - A workaround cites the bug it works around, with a link where one exists.
Tests¶
- The file comment of a test file names the unit under test and what the file covers, and names the fixtures it reads.
TEST_CASEandSECTIONnames are the specification: complete statements of the expected behaviour. They carry no documentation comment; the reference lists them on the test file's page.- Fixtures, helper functions and helper types are documented like production code.
- An expected value that is not obvious says where it comes from: the standard's worked example, an independent decoder, a calculation in Python.
Python¶
- A script starts with its shebang line, if any, then
# SPDX-License-Identifier: GPL-3.0-only, then the module docstring. - Docstrings follow PEP 257 with the Google layout: a
one-line summary in the imperative mood ("Return the ...", as PEP 257 prescribes for
Python), a blank line, a description, then
Args:,Returns:,Yields:andRaises:sections. - The module docstring says what the script does and how to run it. Every function, class and method, private helpers included, has a docstring.
CMake¶
- Every
CMakeLists.txtand.cmakefile starts with# SPDX-License-Identifier: GPL-3.0-onlyand a comment saying what the file builds or defines and how it is used. - Every
function()andmacro()is preceded by a comment describing its arguments, its results and its side effects. A script run withcmake -Plists the-Dvariables it expects. - Every
option()and cache variable has a help string. Non-obvious commands have a comment explaining why.
Workflows¶
- Every workflow starts with
# SPDX-License-Identifier: GPL-3.0-onlyand a comment block saying what the workflow does, what triggers it, and which permissions, secrets and repository variables it needs. - A job whose
namedoes not say what it is for, and a step whose script is not self-explanatory, have a comment explaining why they exist.
Enforcement¶
| Check | Where | What it enforces |
|---|---|---|
Doxygen, warnings as errors (tools/doxygen/Doxyfile, run by mkdocs build) |
Documentation job | Every file, namespace member, type, function, parameter, return value, data member and enumerator in src/ and tests/ is documented, private members and anonymous namespaces included; comments are well formed. |
Clang -Wdocumentation, warnings as errors (cmake/ProjectWarnings.cmake) |
macOS (Clang) job | @param and @tparam names match the declaration; no @return on a void function; commands are well formed. |
File headers (mkdocs build for the C++ file comment, tools/check_file_headers.py for the licence line) |
Documentation job | The licence line in every C++, Python, CMake, workflow and shell file, and a file comment in every C++ file. |
ruff check with the pydocstyle rules of ruff.toml |
Documentation job | Docstrings in the Google layout of PEP 257 in every Python module, class and function, with documented arguments. |
To run the checks locally before pushing:
NMEASIM_REQUIRE_DOXYGEN=1 mkdocs build --strict # Doxygen and the C++ file comments
python3 tools/check_file_headers.py
ruff check . # pip install ruff==0.16.8
Clang's -Wdocumentation runs in every build with Clang or AppleClang, for example the
ci-macos preset.
Checks that a machine cannot make (that a comment is specific, current and explains why) are part of code review.