Skip to content

Latest commit

 

History

History
384 lines (286 loc) · 14.7 KB

File metadata and controls

384 lines (286 loc) · 14.7 KB

Code Style Guide

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++20 Modern Patterns

DART uses C++20 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++20 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++20 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.
  • Next-gen modules (dart/simulation/experimental/, tests/unit/simulation/experimental/, tests/benchmark/simulation/experimental/, and dependents): Use all snake_case file names (e.g., rigid_body.hpp, free_joint.cpp, test_multi_body.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: DART_NAMESPACE_CLASSNAME_HPP_
  • 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
  • Header guards must include the library, namespace, and source file names

Example:

#ifndef DART_EXAMPLE_EXAMPLECLASS_HPP_
#define DART_EXAMPLE_EXAMPLECLASS_HPP_

// 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/SmartPointer.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"

#endif // DART_EXAMPLE_EXAMPLECLASS_HPP_

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:

Element C++ Style Python Style Example (C++) Example (Python)
Functions camelCase snake_case isIdentity() is_identity()
Classes PascalCase PascalCase MyClass MyClass
Variables snake_case snake_case my_variable my_variable
Constants ALL_CAPS ALL_CAPS MY_CONSTANT MY_CONSTANT
Namespaces/Modules N/A snake_case dart::dynamics dart.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

Command Success Failure
pixi run lint No errors, check git status for changes Error messages in output
make check-format "All files formatted correctly" List of files needing formatting
make format Files reformatted silently N/A (always succeeds)

See Also