Code Style Guide

August 1, 2026 · View on GitHub

Start here next time

  • Format all code: pixi run lint
  • Quick conventions: 2-space indent, camelCase functions, PascalCase classes
  • C++ headers: .hpp, sources: .cpp, member vars prefixed with m
  • Python bindings: snake_case functions (PEP 8), PascalCase classes
  • See Quick Reference for the full summary

This document describes the code style conventions used in the DART project.

Table of Contents

C++ Style

C++ headers and sources should be contained in the same subdirectory of dart/ that matches their namespace, with the extension .hpp and .cpp, respectively.

C++23 Modern Patterns

DART uses C++23 features to improve code clarity, maintainability, and performance. Use these patterns when appropriate:

Concepts for Template Constraints:

// Prefer concepts over SFINAE
template <std::floating_point T>
T normalize(T value);

// Custom concepts for domain-specific constraints
template <typename T>
concept UniformIntCompatible = std::integral<T> && !std::same_as<T, bool>;

std::span for Function Parameters:

// Prefer std::span over const std::vector<T>&
void addSkeletons(std::span<const SkeletonPtr> skeletons);

// Accepts vectors, arrays, subranges without temporary allocations

Spaceship Operator for Comparisons:

// Replace 6 operators with 2
auto operator<=>(const Ptr& other) const {
  return std::tie(mT1, mT2) <=> std::tie(other.mT1, other.mT2);
}
bool operator==(const Ptr& other) const = default;

Branch Prediction Attributes:

// Use [[likely]] / [[unlikely]] for hot paths
if (objects.empty()) [[unlikely]] {
  return false;
}

if (!result.isCollision()) [[likely]] {
  return;
}

When NOT to use C++23 features:

  • Avoid std::format until Ubuntu 24.04 LTS (GCC 13+)
  • Don't replace clear index-based loops with complex range expressions
  • Keep SFINAE for Eigen compile-time traits (not proper constexpr)

Modernization summary (DART 7): The codebase now assumes a C++23 baseline, so new read-only APIs should prefer non-owning views (std::span, std::string_view) and call sites should avoid temporary containers. Favor std::ranges algorithms, associative contains, and std::erase(_if) when they clarify intent. Keep modernizations behavior-preserving; avoid deep algorithm rewrites.

File Naming Conventions

DART now uses snake_case for public headers across the core C++ surface while preserving compatibility via generated PascalCase wrappers.

  • Core headers (dart/): Use snake_case for headers (e.g., body_node.hpp, collision_group.hpp). PascalCase compatibility headers are generated by CMake for legacy includes and emit deprecation warnings.
  • Legacy sources (dart/): Keep existing .cpp file names unless you are performing a targeted migration; avoid mass-renames for sources in this release.
  • DART 7 simulation modules (dart/simulation/, tests/unit/simulation/, tests/benchmark/simulation/, and dependents): Use all snake_case file names (e.g., rigid_body.hpp, free_joint.cpp, test_multibody.py). Build-system files such as CMakeLists.txt keep their canonical capitalization.
  • Implementation details: Continue to use the -impl suffix for template implementations (e.g., body_node-impl.hpp, rigid_body-impl.hpp).
  • Tests (tests/): Use snake_case file names with test_ prefix (e.g., test_skeleton_state.cpp, test_collision.cpp). This avoids case-collisions on case-insensitive filesystems (macOS, Windows) and aligns with the DART 7 snake_case convention.

Rationale: The public include surface is moving to snake_case while generated PascalCase wrappers keep downstream builds working. All new code uses snake_case consistently.

Quick Reference

  • Indentation: 2 spaces
  • Functions: camelCase
  • Classes: PascalCase
  • Enum members: PascalCase (e.g., enum class Mode { Disabled, Enabled };)
  • Member variables: Prefixed with m (e.g., mExampleMember)
  • File extensions: .hpp for headers, .cpp for sources
  • File naming: snake_case for all new headers and sources in dart/ and tests/ (PascalCase wrappers generated by CMake for headers only); avoids case-collisions on case-insensitive filesystems (macOS, Windows)
  • Header guards: #pragma once (macro guards only in vendored code and generated files)
  • Braces: No "cuddled" braces for functions/classes (namespaces and control flow are exceptions); control-flow bodies are automatically wrapped in braces by pixi run lint.
  • Documentation: Doxygen-style comments (///)

Parameter Naming

  • New/modified code: Use camelCase names for function parameters (e.g., void setMass(double mass)).
  • Legacy code: Many functions in dart/dynamics still use _param conventions. Avoid mass renames—update these opportunistically when touching the surrounding code, and prefer camelCase for any new APIs.

Header Style

Rules:

  • Use two-space indentation
  • Use camelCase function names
  • Use PascalCase class names
  • No "cuddled" braces for classes or functions!
  • Control-flow bodies are automatically wrapped in braces by pixi run lint
  • Use #pragma once instead of macro include guards; it cannot collide and cannot drift when a file is renamed. pixi run check-header-guards enforces this. Deliberately unguarded files (-impl.hpp textual includes, the poison headers under tests) and vendored upstream code (dart/math/lcp/pivoting/dantzig/, tests/baseline/odelcpsolver/, ikfast.h) are exempt.

Example:

#pragma once

// Place all dependency includes at the top of the file.
// Use absolute paths. Order: Local -> Library -> External -> STL.
#include "dart/common/pointers.hpp"
#include <dart/component_name/ExampleInterface.hpp>
#include <dart/component_name/ExampleOtherInterface.hpp>
#include <library/headers.hpp>
#include <stl_headers>

// Namespaces scopes should be one line each with "cuddled" braces.
namespace dart {
namespace example {

// Use the following macro (defined in dart/common/smart_pointer.hpp) to declare
// STL "smart" pointers. Pointers should be declared ahead of the class so
// that the class itself can use the pointers.
DART_COMMON_DECLARE_SHARED_WEAK(ExampleClass)

/// A required Doxygen comment description for this class. This can be extended
/// to include various useful details about the class, and can use the standard
/// Doxygen tag set to refer to other classes or documentation. It should use
/// the '///' style of block comment.
class ExampleClass : public ExampleInterface, public ExampleOtherInterface
{
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW   // Many classes that require Eigen will also need this macro

  /// Required brief description of constructor. This will often be as simple as:
  /// "Creates an instance of ExampleClass."
  ///
  /// @param[in] foo This is an example parameter description.
  /// @param[in] bar This is a longer example parameter description that needs
  /// to wrap across multiple lines.
  ExampleClass(std::unique_ptr<util::RNG> foo,
               const Eigen::Isometry3d& bar = Eigen::Isometry3d::Identity());

  ExampleClass(const ExampleClass& other);
  ExampleClass(ExampleClass&& other);

  ExampleClass& operator=(const ExampleClass& other);
  ExampleClass& operator=(ExampleClass&& other);

  // If a class should be non-copyable, it should explicitly delete the following:
  ExampleClass(const ExampleClass&) = delete;
  ExampleClass(ExampleClass&& other) = delete;
  ExampleClass& operator=(const ExampleClass& other) = delete;
  ExampleClass& operator=(ExampleClass&& other) = delete;

  // Classes should explicitly declare a default virtual destructor
  // if they do not declare one (unless marking a class as final).
  virtual ~ExampleClass() = default;

  // Documentation inherited.  <-- Use this comment to indicate that the docstring of the interface method applies
  int exampleInterfaceFunction() const override;  // <-- Always explicitly `override` interface functions without `virtual`

  /// Required brief description of method.
  /// @note If a method has output parameters, they should be the last
  /// arguments.
  ///
  /// @param[in] a A description of a
  /// @param[in] b A description of b
  /// @param[out] out A description of out
  int exampleMethod(int a, int b, int* out) const;

private:
  std::unique_ptr<util::RNG> mExampleMember; // Member variables are prefixed with "m"
};

} // namespace example
} // namespace dart

// In certain cases, such as heavily templated code, implementations must be included
// in headers. In this case, a "detail" header should be created in the "./detail"
// subdirectory with the same name as the main header file, but an "-impl" suffix.
// Private declarations in this header can use a "detail" sub-namespace.
#include "dart/component_name/detail/ExampleClass-impl.hpp"

Forward Declarations

Every namespace that needs forward declarations owns exactly one fwd.hpp (dart/common/fwd.hpp, dart/dynamics/fwd.hpp, dart/simulation/fwd.hpp, dart/gui/detail/fwd.hpp, ...). Declare a type once there and #include that header from anywhere that only needs the declaration. Do not hand-roll namespace dart::x { class Foo; } blocks in consuming headers: they drift out of sync and hide who owns the type.

// Wrong: a private copy of somebody else's declaration.
namespace dart::simulation::compute {
class MultibodyContactStage;
} // namespace dart::simulation::compute

// Right.
#include <dart/simulation/fwd.hpp>

Two cases legitimately stay in place, and pixi run lint allows them:

  • In-file ordering. A primary template whose specializations follow in the same file, or any entity the same file goes on to define (for example a class named in a friend declaration before its definition).
  • File-local tag types that are never defined. These need an entry in ALLOWLIST in scripts/check_fwd_headers.py with a comment explaining why.

Third-party namespaces are out of scope, except the render/UI backends used by dart::gui (Filament, GLFW, Dear ImGui), which are declared once in dart/gui/detail/backend_fwd.hpp.

Keep a fwd.hpp cheap: declarations and, at most, the export header. A forward header that pulls in a third-party library brings its names into every consumer and can change overload resolution there. dart/math/fwd.hpp is the exception that proves the rule — it includes Eigen and defines the dart::math Matrix/Vector aliases, so it is a types header, not a declaration header. That is why dart/math/optimization has its own fwd.hpp for its class declarations. Add a subfolder fwd.hpp in the same situation.

pixi run check-fwd-headers enforces this; it also runs as part of pixi run lint and pixi run check-lint.

API Boundary Style

New user-facing API belongs in public module headers and should use the module's component export macro when the symbol crosses a shared-library boundary (DART_API, DART_GUI_API, DART_IO_API, etc.). Implementation details belong under detail/ or internal/; compiled internal symbols should prefer the matching *_LOCAL macro. See api-boundaries.md before adding new public headers, exported classes, or Python bindings.

Source Style

Rules:

  • Use two-space indentation
  • Use camelCase function names
  • Use PascalCase class names
  • No "cuddled" braces for classes or functions!
  • Control-flow bodies are automatically wrapped in braces by pixi run lint
  • Each function is separated by an 80 column line of "=" characters

Example:

// Includes should be at the top of the file.
// The first include in a class source file should be the matching `.hpp` header file.
#include "dart/example/ExampleClass.hpp"

#include <stl_headers>
#include <library/headers.hpp>
#include "dart/example/OtherHeaders.hpp"

// Namespace nesting is preferred to "using namespace" directives.
// Namespaces scopes should be one line each with "cuddled" braces.
namespace dart {
namespace example {

// Each function is separated by an 80 column line of "=" characters.
//==============================================================================
int ExampleClass::exampleInterfaceFunction() const
{
  if (mExampleMember) {
    return 3;
  }

  return -1;
}

//==============================================================================
int ExampleClass::exampleMethod(int a, int b, int* out) const
{
  int result = a + b;
  if (out) {
    *out = result;
  }
  return result;
}

} // namespace example
} // namespace dart

Exception Handling

Never swallow exceptions silently. When catching exceptions for logging, always re-throw to propagate the error:

// CORRECT: Log and re-throw
try {
  value = std::stod(str);
} catch (const std::exception& e) {
  DART_ERROR("Failed to parse '{}': {}", str, e.what());
  throw;  // Re-throw to propagate the error
}

// WRONG: Silently swallow the exception
try {
  value = std::stod(str);
} catch (const std::exception& e) {
  DART_ERROR("Failed to parse '{}': {}", str, e.what());
  // Missing throw! Execution continues with uninitialized data
}

Rationale: Silently swallowing exceptions can lead to subtle bugs where code continues with uninitialized or partial data. Always propagate errors to the caller so they can handle them appropriately.

Smart Pointers

These guidelines are based on Herb Sutter's article. Consider looking at the full article for details.

General Rules:

  • Use a by-value std::shared_ptr as a parameter if the function surely takes the shared ownership.
  • Use a const std::shared_ptr& as a parameter only if you're not sure whether or not you'll take a copy and share ownership.
  • Use a non-const std::shared_ptr& parameter only to modify the std::shared_ptr.
  • Use std::unique_ptr anytime you want to use a std::shared_ptr but don't need to share ownership.
  • Otherwise use Object* instead, or Object& if not nullable.

Macro Conventions

All macros in DART are prefixed with DART_ to distinguish them from other identifiers:

  • DART_HAS_<optional_dep>: Boolean set to true when an optional dependency is detected
  • DART_ENABLE_<optional_feature>: Boolean set to true if the optional feature should be enabled when requirements are met
  • DART_ENABLED_<optional_feature>: Boolean set to true if the optional feature is enabled

Use all-caps for all macro names to ensure consistency and to visually distinguish macros from other variables.

Python Style (dartpy bindings)

Naming Conventions

The Python bindings use different naming conventions than the C++ code to follow Python community standards:

ElementC++ StylePython StyleExample (C++)Example (Python)
FunctionscamelCasesnake_caseisIdentity()is_identity()
ClassesPascalCasePascalCaseMyClassMyClass
Variablessnake_casesnake_casemy_variablemy_variable
ConstantsALL_CAPSALL_CAPSMY_CONSTANTMY_CONSTANT
Namespaces/ModulesN/Asnake_casedart::dynamicsdart.dynamics

Example

C++ code:

auto so3 = SO3();
bool is_identity = so3.isIdentity();

Python code:

so3 = SO3()
is_identity = so3.is_identity()

Rationale

Using different naming conventions allows each API to follow its language's community standards:

  • camelCase is more common for function names in the C++ community
  • snake_case is more common for function names in the Python community

This makes the code more readable and easier to integrate with other projects in each ecosystem, while maintaining consistency within each language.

CMake Style Summary

  • Indentation: 2 spaces
  • Functions: lowercase
  • Variables: ALL_CAPS (except target names)
  • Target variables: target_VARIABLE format
  • Quoting: Always quote singleton variables ("${MY_VAR}"), but not lists

See CONTRIBUTING.md for detailed examples.

Auto-formatting

Use ClangFormat to automatically format C++ code:

cd build/
make check-format  # Check without modifying
make format        # Apply formatting

Signals to Look For

CommandSuccessFailure
pixi run lintNo errors, check git status for changesError messages in output
make check-format"All files formatted correctly"List of files needing formatting
make formatFiles reformatted silentlyN/A (always succeeds)

See Also