The coding standard for all C++ in RoadMaker. Read this before writing or reviewing kernel, editor, or binding code — it defines the error-handling, ownership, and API patterns every contribution must follow.
RoadMaker is C++20 with no compiler extensions (see ADR 0001 for why). CI builds with warnings-as-errors on all three platforms, and formatting is enforced with clang-format:
git clang-format- Public kernel APIs return
rm::Expected<T>— an alias ofstd::expectedwhere available, otherwise the pinnedtl::expected(the kernel targets C++20;std::expectedis C++23). The error type isrm::Error { code, message, context }. - Exceptions may exist inside a function body, but must never cross the
public kernel API, the C boundary, or the nanobind bindings. Catch and
convert to
rm::Errorat the boundary. - Parsers do not throw and do not stop at the first problem: they accumulate
std::vector<rm::Diagnostic>(severity, XPath-like location, message) and keep going. They fail only on structural errors that make further parsing meaningless.
- Domain objects live in arenas owned by
RoadNetwork. Cross-references between domain objects are generational strong IDs, never raw pointers: therm::Id<Tag>template (core/include/roadmaker/road/id.hpp) carries an{index, generation}pair, withRoadId,LaneId,JunctionId, etc. as tagged instantiations. - Lookup goes through the network:
network.road(id)returnsRoad*, ornullptrif the ID is stale (the slot was freed or reused). Callers must handle the null case. - Never store references or pointers to arena objects across mutations — any mutation of the network may invalidate them. Re-look-up by ID instead.
- Prefer free functions over member functions when they don't need private state. This keeps classes small and APIs composable.
- Take non-owning parameters as
std::span/std::string_view. Never sink references to temporaries.
- Everything lives in
namespace roadmaker, with the project-wide aliasnamespace rm = roadmaker. - Files:
snake_case.hpp/snake_case.cpp. One class per header where reasonable.#pragma oncein every header. - Types and template parameters:
PascalCase. Functions and variables:snake_case. Constants:kPascalCase. Data members: trailingunderscore_. - Include order: the matching header first, then project headers, then third-party, then the standard library.
Prefer:
- Designated initializers for options/config structs.
- Ranges where they genuinely simplify the code.
constexprwherever the computation allows it.[[nodiscard]]on everyExpected-returning API.enum class, always — never unscoped enums.
Avoid:
- Raw
new/delete(use containers and smart pointers; arena objects are owned by their arena). - Out-parameters — return a struct instead.
- Inheritance for code reuse — use composition; inheritance is for genuine interfaces (e.g., the renderer abstraction).
iostreamanywhere incore/— use fmt for formatting and spdlog for logging.- Macros and singletons.
Concurrency stance: the kernel is single-threaded per RoadNetwork. Internal
parallelism (e.g., per-road meshing via std::for_each with a parallel
execution policy) is fine as long as the API stays externally single-threaded.
doubleeverywhere in geometry.floatappears only in render-facing mesh buffers, at the last conversion step.- Tolerances are named constants in
rm::tol(core/include/roadmaker/tol.hpp) — e.g.,tol::kLength = 1e-6m for length comparisons,tol::kAngle = 1e-9rad for heading continuity. Never inline a magic epsilon; if no existing constant fits, add one totol.hppwith a doc comment stating its unit and purpose. - Every public geometric API documents its units and reference frame in its doc comment. The kernel frame is right-handed, Z-up, meters, radians (the OpenDRIVE convention) — see the architecture overview for where conversions are allowed.
Style and tests are inseparable: tests are written with the code, not after. Geometry code ships golden analytic tests plus property-style checks (e.g., arc-length monotonicity, curvature continuity at G1 joints), and round-trip invariants (author → write → parse → compare) are first-class tests. The full testing doctrine — frameworks, structure, and what CI runs — lives in contributing/testing.