Style and conventions for working in the DOLFINx repository. This complements CONTRIBUTING.md, which covers the PR/AI disclosure process.
cpp/dolfinx/— the C++ library, organized by sub-namespace (common,mesh,fem,graph,la,io,nls,refinement,geometry).cpp/test/— C++ unit tests (Catch2 3).cpp/demo/— C++ demo programs.python/dolfinx/— the Python package.python/dolfinx/wrappers/— nanobind C++/Python bindings.python/demo/,python/test/— Python demos and pytest suite.
-
Standard: Modern C++20. Use concepts (
std::floating_point T,std::integral,std::rangesetc.) to constrain templates rather than SFINAE. Don't use C-style casts.const-correctness is encouraged. Consider usingconstexprandconsteval. -
Avoid overusing the
autokeyword: Theautokeyword should not be used on simple-to-reason-about types, e.g.std::int32_tandstd::vector<T>as it reduces code readability. -
Formatting: enforced by
.clang-format(LLVM-derived, 2-space indent, 80-column limit, Allman braces). Always runclang-format -ion touched.cpp/.hfiles before considering a change done; CI runsclang-format --dry-run --Werrorand will fail otherwise. -
File header: every source file starts with
// Copyright (C) <years> <author(s)> // // This file is part of DOLFINx (https://www.fenicsproject.org) // // SPDX-License-Identifier: LGPL-3.0-or-later
Add your name/year to the existing copyright line when you make a substantive change; don't replace existing authors.
-
Headers: use
#pragma once, not include guards. Includes are sorted (clang-format'sSortIncludes) — the paired header first (in.cppfiles), then project (<dolfinx/...>) headers, then standard-library headers, alphabetically within each group. -
Include What You Use (IWYU): follow IWYU best practice down to including 'trivial' headers such as
<cstdint>and<iterators>directly, rather than relying on transitive includes -- IWYU is not currently enforced systematically via testing, so check touched files for missed opportunities and suggest fixes. -
Namespaces: library code lives in
dolfinx::<module>(e.g.dolfinx::io::hdf5). In.cppfiles, preferusing namespace dolfinx;at the top and qualify definitions with the remaining namespace path, e.g.io::hdf5::open_file(...)rather than fully qualifying every symbol. -
Naming:
snake_casefor functions and variables,PascalCasefor types/classes, private/protected data members prefixed with an underscore (_dofmap,_index_map_bs). -
Integer types:
std::int32_tfor process-local indices and local offsets,std::int64_tfor global indices and global offsets,intfor MPI ranks, counts and displacements,std::size_tfor.size()results. The 32-bit local index is a deliberate commitment — a rank is not expected to exceed 2^31 entities, and the narrow type halves index array memory traffic — so local indices must not be silently widened in storage or interfaces. Type a variable by the role of its value, not by the expression that initialises it. -
PETSc/SLEPc index types: in the thin
la::petsc/la::slepcwrappers, an index or count passed straight through to a PETSc/SLEPc call isPetscInt, matching thePetscScalar/Mat/Vecalready in those signatures. The fixed-width types above are for DOLFINx-meaningful quantities, such as the sizes and ranges returned bypetsc::Vector. -
Iterator distances: store
std::distanceresults, a signeddifference_type, instd::size_twhen used as a container offset. They are non-negative by construction here, and-Wsign-compareis-Werror, sostd::ptrdiff_twould force a cast at every comparison against.size(). -
Narrowing conversions: neither
-Wconversionnor-Wshorten-64-to-32is enabled, so implicit 64-to-32 narrowing is legal and widespread.static_castonly where the narrowing is the point — a public API returning a local index or count (IndexMap::size_local,IndexMap::num_ghosts) — not elsewhere. -
Parameters: pass read-only strings as
std::string_view, notconst std::string&. Usestd::spanfor contiguous read-only array views, andmdspanfor read-only multi-dimensional views. Reservestd::vector/std::stringfor parameters that are stored, mutated in place, or are container element types. -
Documentation: Doxygen-style
///comments above declarations. Use@brieffor anything non-trivial,@param[in]/@param[in,out]per parameter,@return,@note. Keep@paramnames in sync with the actual parameter names — this is checked manually in review, not by tooling, so a rename must be applied to the declaration, the definition, and any doc comment together. -
Comments: LLM-generated comments tend to be rather verbose; after the first comment draft, compress comments to their essence using concise technical language.
-
Errors and invariants: For user-facing/API-boundary errors, throw
std::runtime_errorwith a descriptive message — unconditionally when the check is O(1), or guarded behind#ifndef NDEBUGwhen the check is more expensive, so it's skipped in release builds. For internal invariants that indicate a library bug rather than bad user input, useassertwhen the check fits in a single expression, or a#ifndef NDEBUG-guarded block with an explicit throw/abort when it needs multiple statements. Do not add exceptions inside hot loops. Preferspdlog::debug/info/warnfor logging overstd::cout/std::cerr. -
MPI collectives: collective operations (
MPI_Allreduce, neighbourhood collectives, etc.) must be reached by every rank in the communicator — an error path, early return, or exception on one rank must not skip a collective that other ranks still call, or the mismatch deadlocks. Validate/throw before entering a code path with collectives, not conditionally partway through it. -
Move/copy semantics: Moving is preferred over copying, unless the object is very lightweight. Many DOLFINx classes disable copying; none disable moving.
std::moveis used systematically on incomingstd::shared_ptrto avoid unnecessary copies ofstd::shared_ptrand also to avoid copies when returning with astd::pair{}. -
Special member functions: a class that declares any of the five (copy constructor, move constructor, destructor, copy assignment, move assignment) declares all five explicitly,
public, as a contiguous block immediately after the named constructors, in that order — seemesh/Mesh.h,fem/DofMap.h,common/Table.h. Write= defaultrather than relying on implicit generation; declaring only some of the five silently suppresses the others. -
Deleting copies: delete both copy operations for classes owning an external handle (MPI communicator, PETSc/SLEPc object, ADIOS2/HDF5 file) and for 'heavy' data classes where an accidental deep copy is a performance bug (
common::IndexMap,fem::FunctionSpace,fem::Function). Classes that are cheap to copy explicitly but should not be copied into an existing object delete copy assignment only, keeping a defaulted copy constructor (mesh::Mesh,mesh::Topology,mesh::Geometry). -
Never delete moves: no class in the library deletes a move operation. If a class caches
std::spans or other pointers into its own members, explain in a@notewhy moving remains valid instead of disabling it (see the deleted copy constructor and defaulted move constructor offem::Form). -
Destructors:
= defaultunless a raw handle must be released (common::Comm,la::petsc::Vector,io::VTKFile). Mark the destructorvirtualonly when the class is actually used as a base class. -
noexcept: used on hand-written move operations of handle owners (common::Comm,la::petsc::*); defaulted moves are left unannotated as they are implicitlynoexcept. -
Documenting special members: the conventional wordings are
/// Copy constructor,/// Move constructor,/// Destructor,/// Copy assignmentand/// Move assignment;@paramis normally omitted. Deleted members use a non-Doxygen//comment with(deleted)appended so they stay out of the generated documentation. -
String formatting: use
std::format(<format>) to build formatted/error strings rather thanprintf-style,std::ostringstreamconcatenation, or thefmtlibrary. -
Function pointers over lambdas: when a free function's signature already matches a callback/
std::functionparameter exactly, pass the function directly (e.g.graph::reorder_rcm) rather than wrapping it in a trivial forwarding lambda. -
Lambdas: no
[=]/[&]— list captures explicitly, e.g.[&v]. Capture by reference for lambdas invoked in place; by value (cheap scalars, or[v = std::move(v)]to move a container) for lambdas that escape the scope, where a captured reference orspaninto a local dangles silently.[this]only if the lambda cannot outlive the object. Never capture a container orshared_ptrby value for convenience: the copy happens at capture and again whenever the lambda or itsstd::functionis copied. Prefer explicit parameter types overauto(auto&&in generic code); add an explicit return type when the deduced one is non-obvious or must not decay. Avoidmutable. Promote long or reused lambdas to a free function in an anonymous namespace. A by-reference capture isconstonly if the captured variable is, so declare read-only localsconst. -
Algorithms: prefer
std::rangesalgorithms (std::ranges::...) over both hand-written loops and their pre-ranges<algorithm>equivalents, where it doesn't hurt clarity or performance. Flattened row-major storage is the default convention for multi-dimensional data passed as flat buffers. -
std::distance/std::advance/std::next/std::prevon a generic, template-parameterized range: these legacy<iterator>algorithms dispatch onstd::iterator_traits<It>::iterator_category, not the C++20 iterator concepts. Views such asstd::ranges::iota_viewsatisfystd::random_access_iteratorbut not the legacyLegacyRandomAccessIterator(operator*returns a prvalue, not a reference), so theiriterator_categorydegrades toinput_iterator_tagandstd::distancesilently falls back to an O(n) count instead of an O(1) subtraction, turning an O(n) loop into O(n²) in unoptimised (Debug) builds only. When indexing into a generic range parameter inside a loop, usestd::ranges::distance/std::ranges::advance, which dispatch on the C++20 concept and stay O(1) for these views, or track the index with a plain counter incremented alongside the iterator. -
Windows: Windows is continuously tested on GitHub with the most important missing feature being the lack of C99
_Complexsupport denoted by existence ofDOLFINX_NO_STDC_COMPLEX_KERNELSmacro variable. -
PETSc support is optional: Tests should avoid depending on PETSc unless the functionality under test is PETSc-specific; PETSc-related functionality in the main library should be isolated so it can be excluded from non-PETSc builds. For example, the test suite includes a simple conjugate-gradient solver for small problems rather than depending on a PETSc
KSPsolver.
- Formatting/linting:
ruff(bothruff checkandruff format --check), configured inpython/pyproject.toml. Line length 100, 4-space indent. Rule set includes pydocstyle (D, Google convention), pycodestyle, pyflakes, isort, pyupgrade, flake8-import-conventions, and NumPy-specific rules. - Import order (via ruff's isort): future → standard-library →
mpi4py/petsc4py(ownmpisection) → third-party → first-party (basix,dolfinx,ffcx,ufl) → local-folder. - Docstrings: Google style (
Args:,Returns:, etc.), module and public API documented; test/demo files are exempt from some pydocstyle rules (seeper-file-ignores). - Type hints: required on the public API; checked with
mypy(python/pyproject.toml[tool.mypy]config, run overdolfinx,test, anddemo). PETSc-related type checking is disabled on a per-line basis until upstream petsc4py type work is finished. - File header: same SPDX/copyright block as C++, adapted to
#comments, followed by a module docstring.
- One file per C++ module (
fem.cpp,mesh.cpp,la.cpp, ...), wired together fromdolfinx.cpp. - Bind free functions with
m.def(...), giving named arguments vianb::arg("name")matching the C++ parameter name. Check the ordering matches as it's easy to make a mistake. - Wrap C++ types with
nb::class_<T>(m, "Name", "docstring"), chaining.def(...),.def_prop_ro(...),.def_ro(...). - These files are still C++:
clang-formatapplies to them too (CI checkspython/dolfinx/wrappersseparately). - Do not give bound arguments Python-visible default values
(
nb::arg("x") = value) in the C++ nanobind wrapping code — defaults belong in the pure-Python layer. - The nanobind wrappers are further wrapped into a pure-Python
interface which contains the user facing API. Users and developers
are discouraged from using
dolfinx.cppdirectly.
- Formatted with
gersemi(2-space indent, see.gersemirc); CI runsgersemi --check ..
- C++ demos are written with Markdown comments for subsequent postprocessing with jupytext and sphinx.
- Python demos are written with light format and Markdown for subsequent postprocessing with jupytext and sphinx.
- Demo text should be checked for clarity, brevity, mathematical correctness (e.g. missing definitions) and misalignment with the presented solver code.
- Developer vs Release build mode: Both C++
cmake ... -DCMAKE_BUILD_TYPE=Developerand Python partspip ... -Ccmake.build-type=Developermust be built in Developer mode which enables hardened debugging/correctness checks. Performance profiling must be done on a build built withReleasemode. - C++: Catch2 3, in
cpp/test/. FFCx-generated forms are compiled as part of the test build (seecpp/test/CMakeLists.txt). - Python:
pytest, inpython/test/. Usempi4py.MPIfixtures for parallel-aware tests where relevant. - Run the relevant formatter/linter and the affected test suite before calling a change done — don't rely on CI to catch formatting.
- Dependency groups (
build,docs,lint,test,ciinpython/pyproject.toml) use PEP 735 syntax and requirepip >= 25.1(or another PEP 735-compliant build frontend) for the--groupflag.demo,optional,petsc4py, andtypingremain real[project.optional-dependencies]extras since they are user-facing runtime features, or (in the case oftest) are installed against built wheels where dependency groups are unavailable.
- C++: configure once with CMake+Ninja against an installed Basix,
then
ninjain the build directory is enough to catch real compilation errors (the project builds with-Werror). Don't trust editor/clangd diagnostics alone — they're frequently noise from incomplete include paths, not real errors. - Python: To build the Python interface the C++ interface must be
ninja installed. - Prefer
clang-format --dry-run --Werror/ruff check/ruff format --check/gersemi --checklocally to match exactly what CI enforces, rather than eyeballing style.