From 4914745e6c0fba8df7ad57a65e0af532361dd7fb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:42:56 +0000 Subject: [PATCH 1/2] build: bump numerical-toolbox to 63c9b91 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up NT53–NT65 (now shipped upstream), the NT66 SE(2) spec, and upstream's protected, non-virtual interface destructors. The roadmap note about the pin is updated to match. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Mmp77ieEy16GxJL8KsLsVx --- CMakeLists.txt | 2 +- ROADMAP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34dde3b..4edcc76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,7 @@ if (NOT TARGET numerical.math) FetchContent_Declare( numerical_toolbox GIT_REPOSITORY https://github.com/embedded-pro/numerical-toolbox-cpp.git - GIT_TAG adfc10d342591df8e46a0b1f0a91329bc76023e0 # main + GIT_TAG 63c9b91f4f6f1b306bc15fbf59d01a106da02697 # main ) set(NUMERICAL_TOOLBOX_ENABLE_OPTIMIZATIONS ${ROBOTICS_TOOLBOX_ENABLE_OPTIMIZATIONS} CACHE BOOL "" FORCE) diff --git a/ROADMAP.md b/ROADMAP.md index c5ef9d1..ee6fb26 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -41,7 +41,7 @@ this roadmap needs that upstream does not ship yet are specified and built upstr Generic algorithms live in numerical-toolbox-cpp and are referenced here as **NT** plus its master-list item number (NT53 is upstream item 53). A robotics item that depends on one needs a numerical-toolbox pin that ships it. NT53–NT65 have shipped upstream, and their links point to the upstream -docs; using them needs a pin at or after `1f6d717` (the current pin, `adfc10d`, predates them). +docs; the current pin (`63c9b91`) includes them. | # | Component | Upstream module | Spec / doc | Needed by | |------|-------------------------------------------------------|----------------------|-----------------------------------|----------------------------------------------------------------------------| From d0c9cd9129e19314063ca545e04e1dd1ad694061 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:42:56 +0000 Subject: [PATCH 2/2] perf: make interface destructors protected and non-virtual Same change as numerical-toolbox-cpp#361. NewtonEulerBody and EulerLagrangeDynamics declared `virtual ~I() = default`. That puts a deleting destructor in every implementation's vtable, which links operator delete, and with it malloc/free, into heap-free firmware. Nothing in the library, the tests or the simulator destroys an object through these interfaces, so both now have a protected, non-virtual destructor. Deleting through an interface pointer becomes a compile error. The rule is updated in AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, the C++ instructions and the reviewer agent. The roadmap specs that declare new interfaces (JacobianProvider, InverseDynamicsModel, InertialRegressor, PathGeometry, JointLimits) follow it too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Mmp77ieEy16GxJL8KsLsVx --- .github/agents/reviewer.agent.md | 2 +- .github/copilot-instructions.md | 2 +- .github/instructions/robotics-cpp.instructions.md | 2 +- AGENTS.md | 3 ++- CLAUDE.md | 2 +- roadmap/dynamics/ChainDynamicsModel/implementation.md | 2 +- .../dynamics/CoriolisMatrixAndRegressor/implementation.md | 2 +- roadmap/kinematics/GeometricJacobian/implementation.md | 2 +- .../TimeOptimalPathParameterization/implementation.md | 4 ++-- robotics/dynamics/EulerLagrangeDynamics.hpp | 5 +++-- robotics/dynamics/NewtonEulerBody.hpp | 5 +++-- 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/agents/reviewer.agent.md b/.github/agents/reviewer.agent.md index 4f9aedc..4ef4f7e 100644 --- a/.github/agents/reviewer.agent.md +++ b/.github/agents/reviewer.agent.md @@ -56,7 +56,7 @@ End with totals + verdict: APPROVE / REQUEST CHANGES. - [ ] SOLID: one concern per class, constructor injection, depend on abstractions, no duplicated logic. **Interfaces & errors (WARNING)** -- [ ] `virtual ~I() = default` — never `= 0`. No exceptions — `std::optional`/status enums. +- [ ] Interfaces: `protected:` non-virtual `~I() = default` — no `virtual` or `= 0` destructor, no ownership or deletion through an interface pointer. No exceptions — `std::optional`/status enums. **Testing (WARNING)** - [ ] `TEST_F` on `float` — no `TYPED_TEST`, no multi-type, never plain `TEST()`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 56f8ed7..312c2e2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,5 +9,5 @@ Essentials (full detail in AGENTS.md): - **Embedded** — no `#pragma GCC optimize` / `optimize` attribute (they block inlining); the consumer sets optimization flags per TU; `OPTIMIZE_FOR_SPEED` (forced inlining) on hot paths. - **No comments** (except license/NOLINT). Allman braces, brace-init, PascalCase/camelCase. - **Tests** — `TEST_F` on `float`, `StrictMock` only, never plain `TEST()`, no redundant cases. -- **No exceptions** — `std::optional`/status enums; interfaces `virtual ~I() = default`. +- **No exceptions** — `std::optional`/status enums; interfaces get a `protected:` non-virtual `~I() = default`, never a virtual destructor. - **Be terse** — minimal prose; report file paths + pass/fail. diff --git a/.github/instructions/robotics-cpp.instructions.md b/.github/instructions/robotics-cpp.instructions.md index 49d8225..be802f9 100644 --- a/.github/instructions/robotics-cpp.instructions.md +++ b/.github/instructions/robotics-cpp.instructions.md @@ -56,7 +56,7 @@ Use `math::IsFinite` for finiteness checks: a consumer may still build with `-ff - SOLID principles — constructor injection, depend on abstractions - DRY — extract shared logic into helpers or templates - RAII for resource management -- **No pure virtual destructors**: Never declare `virtual ~Interface() = 0`. Pure virtual destructors force the compiler to emit a thunk and a separate body, wasting memory on embedded targets. Use `virtual ~Interface() = default` if a virtual destructor is required, or omit it entirely when the class is never deleted through a base pointer. +- **Protected, non-virtual interface destructors**: declare `~Interface() = default` in a `protected:` section, never `virtual ~Interface()` (defaulted or `= 0`). A virtual destructor puts a deleting destructor in every implementation's vtable, which links `operator delete` (and with it `malloc`/`free`) into heap-free firmware, and under multiple inheritance adds a thunk per base. The protected destructor makes deletion through an interface pointer a compile error. Concrete classes declare no destructor unless they need one, and callers hold the concrete type rather than a `std::unique_ptr` to the interface. - No virtual calls in ISR-callable or real-time critical paths ## Documentation — MANDATORY diff --git a/AGENTS.md b/AGENTS.md index debaafc..ffee2f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,8 @@ and included as `numerical//…`. ## Interfaces & errors -- Interfaces = pure virtual classes; `virtual ~I() = default` — **never** `= 0` destructors. +- Interfaces = pure virtual classes with a `protected:`, non-virtual `~I() = default` — **never** a `virtual` or `= 0` destructor. A virtual one puts a deleting destructor in every vtable, which links `operator delete`/`free` into heap-free firmware. +- Concrete classes declare no destructor unless they need one. Never own or delete through an interface pointer (no `std::unique_ptr`): hold the concrete type, pass `I&`. - No exceptions. Use `std::optional` or status enums. `assert`/`really_assert` for preconditions. - SOLID + DIP: constructor injection; depend on abstractions. RAII. No virtual calls in real-time paths. diff --git a/CLAUDE.md b/CLAUDE.md index 15a247e..e5c68c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,5 +10,5 @@ Essentials (full detail in AGENTS.md): - **Embedded** — no `#pragma GCC optimize` / `optimize` attribute (they block inlining); the consumer sets optimization flags per TU; `OPTIMIZE_FOR_SPEED` (forced inlining) on hot paths. - **No comments** (except license/NOLINT). Allman braces, brace-init, PascalCase/camelCase. - **Tests** — `TEST_F` on `float`, `StrictMock` only, never plain `TEST()`, no redundant cases. -- **No exceptions** — `std::optional`/status enums; interfaces `virtual ~I() = default`. +- **No exceptions** — `std::optional`/status enums; interfaces get a `protected:` non-virtual `~I() = default`, never a virtual destructor. - **Be terse** — minimal prose; report file paths + pass/fail. diff --git a/roadmap/dynamics/ChainDynamicsModel/implementation.md b/roadmap/dynamics/ChainDynamicsModel/implementation.md index 8ba9753..e5fe2a4 100644 --- a/roadmap/dynamics/ChainDynamicsModel/implementation.md +++ b/roadmap/dynamics/ChainDynamicsModel/implementation.md @@ -11,7 +11,7 @@ library turns a link description into those interfaces. This adapter does: it im ```cpp template # static_assert(std::is_floating_point_v); instantiated for float -class InverseDynamicsModel: # new interface, virtual ~InverseDynamicsModel() = default +class InverseDynamicsModel: # new interface, protected ~InverseDynamicsModel() = default virtual StateVector ComputeInverseDynamics(const StateVector& q, const StateVector& qDot, const StateVector& qDDot) const = 0 diff --git a/roadmap/dynamics/CoriolisMatrixAndRegressor/implementation.md b/roadmap/dynamics/CoriolisMatrixAndRegressor/implementation.md index e6f155f..f48e066 100644 --- a/roadmap/dynamics/CoriolisMatrixAndRegressor/implementation.md +++ b/roadmap/dynamics/CoriolisMatrixAndRegressor/implementation.md @@ -24,7 +24,7 @@ class CoriolisMatrix: # stateless JointVector TransposeTimesVelocity(links, q, q̇) const # Cᵀ(q, q̇) q̇ template -class InertialRegressor: # interface, virtual ~InertialRegressor() = default +class InertialRegressor: # interface, protected ~InertialRegressor() = default virtual Matrix Compute(q, q̇, q̇r, q̈r) const = 0 # Y with Y·π = M q̈r + C q̇r + g template diff --git a/roadmap/kinematics/GeometricJacobian/implementation.md b/roadmap/kinematics/GeometricJacobian/implementation.md index 27d095c..92869e5 100644 --- a/roadmap/kinematics/GeometricJacobian/implementation.md +++ b/roadmap/kinematics/GeometricJacobian/implementation.md @@ -18,7 +18,7 @@ class GeometricJacobian: # stateless template # DI seam used by all task-space controllers class JacobianProvider: - virtual ~JacobianProvider() = default + protected ~JacobianProvider() = default # non-virtual; never deleted through the interface virtual math::Matrix Jacobian(const JointVector& q) const = 0 virtual math::Vector BiasAcceleration(const JointVector& q, const JointVector& qDot) const = 0 # J̇·q̇ virtual SE3Transform ToolPose(const JointVector& q) const = 0 diff --git a/roadmap/trajectory/TimeOptimalPathParameterization/implementation.md b/roadmap/trajectory/TimeOptimalPathParameterization/implementation.md index 9d3e89e..572a580 100644 --- a/roadmap/trajectory/TimeOptimalPathParameterization/implementation.md +++ b/roadmap/trajectory/TimeOptimalPathParameterization/implementation.md @@ -14,7 +14,7 @@ definition: CubicSplineTrajectory spec → Data structures). template using JointVector = math::Vector template # static_assert(std::is_floating_point_v); instantiated for float -class PathGeometry: # DI, virtual ~PathGeometry() = default — fixed path q(s), s ∈ [0, 1] +class PathGeometry: # DI, protected ~PathGeometry() = default — fixed path q(s), s ∈ [0, 1] virtual JointVector Position(T s) const = 0 # q(s) virtual JointVector FirstDerivative(T s) const = 0 # q'(s) virtual JointVector SecondDerivative(T s) const = 0 # q''(s) @@ -29,7 +29,7 @@ struct JointBounds: JointVector velocityMax # q̇max > 0 template -class JointLimits: # DI, virtual ~JointLimits() = default +class JointLimits: # DI, protected ~JointLimits() = default virtual PathCoefficients Coefficients(T s) const = 0 virtual const JointBounds& Bounds() const = 0 diff --git a/robotics/dynamics/EulerLagrangeDynamics.hpp b/robotics/dynamics/EulerLagrangeDynamics.hpp index 5819d22..91ac822 100644 --- a/robotics/dynamics/EulerLagrangeDynamics.hpp +++ b/robotics/dynamics/EulerLagrangeDynamics.hpp @@ -15,10 +15,11 @@ namespace dynamics using StateVector = math::Vector; using MassMatrix = math::SquareMatrix; - virtual ~EulerLagrangeDynamics() = default; - virtual MassMatrix ComputeMassMatrix(const StateVector& q) const = 0; virtual StateVector ComputeCoriolisTerms(const StateVector& q, const StateVector& qDot) const = 0; virtual StateVector ComputeGravityTerms(const StateVector& q) const = 0; + + protected: + ~EulerLagrangeDynamics() = default; }; } diff --git a/robotics/dynamics/NewtonEulerBody.hpp b/robotics/dynamics/NewtonEulerBody.hpp index 295ec7f..006272b 100644 --- a/robotics/dynamics/NewtonEulerBody.hpp +++ b/robotics/dynamics/NewtonEulerBody.hpp @@ -14,9 +14,10 @@ namespace dynamics using Vector3 = math::Vector; using InertiaMatrix = math::SquareMatrix; - virtual ~NewtonEulerBody() = default; - virtual T ComputeMass() const = 0; virtual InertiaMatrix ComputeInertia() const = 0; + + protected: + ~NewtonEulerBody() = default; }; }