07-litewing-cpp-course-evaluation
Fable evaluation — C++ for LiteWing
BLUF
This is a genuinely good, safety-first course with clean, warning-clean C++20 that compiles and tests green (verified 2026-07-15 on clang 21, and still clean under stricter -Wconversion -Wsign-conversion -Wshadow -Wdouble-promotion). Its standout is the hardware-gate discipline. Its main gap as a modern C++ course is that the code is written in a conservative "C++03-with-std::array" style: it under-uses the four idioms an embedded-C++-2026 course should be built around — constexpr/noexcept, strong types on the hot path (chrono for dt), designated initializers, and value-based error signaling expected/optional. There is also one real testing footgun. Fixes below are prioritized.
Scorecard
| Dimension | Grade | Note |
|---|---|---|
| Pedagogical structure & safety gating | A | Module→lab→boundary-evidence loop and the Module 06 hardware gate are exemplary |
| Build hygiene | A− | C++20, extensions off, -Werror; misses a few high-value warnings + sanitizers |
| Correctness of the code as written | A | Verified clean compile + passing tests; allocation-free; explicit ctors |
| Modern C++ idiom adoption | C+ | The teaching gap — see findings M1–M6 |
| Testing methodology | C | assert-in-main silently no-ops under -DNDEBUG (proven below) |
| Honesty / sourcing | A | Scope, blocked states, and S1–S3 citations are disciplined |
What's already right (keep)
- Allocation-free, no raw
new/delete, no owning raw pointers → correct for the domain, already aligned with embedded modern C++. explicitconstructors, default member initializers (float value_{};),std::array<float,4>over C arrays.- Warnings-as-errors +
CMAKE_CXX_EXTENSIONS OFF. 0.0Ffloat literals (avoids silent double promotion) — a real embedded positive.
Findings & recommendations (prioritized)
M1 — Make the core constexpr + noexcept (highest-value modern gap)
None of clamp, mix_x, LowPass, or Pid are constexpr or noexcept. On ESP-IDF exceptions are off by default, so noexcept is free and honest; and constexpr lets the mixer/filter math fold at compile time and live in ROM. This is the idiom an embedded C++20 course should lead with.
- Mark
clampandmix_xconstexpr noexcept; makeLowPass/Pidmethodsconstexprwhere state allows andnoexceptthroughout. - Payoff beyond runtime: some unit tests become
static_assert— zero-runtime, and they can't be compiled out (see M-test).
M2 — Resolve the strong-types-vs-naked-float contradiction (dt)
Module 02 preaches "a type prevents treating a time interval as a motor command," but Pid::step(float, float, float dt_seconds) and LowPass::update(float) take naked floats, and the declared ImuRate struct is never used. The course argues its own thesis and then doesn't apply it.
- Take
dtasstd::chrono::duration<float>(or aSecondsstrong type). Invalid-dtrejection then falls out of the type system, and it's the single most convincing "modern C++ = fewer bugs" lesson you can give. - Either wire
ImuRateintoLowPass::updateor delete it; a dead strong type undercuts the point.
M3 — Signal invalid input instead of silently swallowing it
Two silent behaviors work against the course's stated principles:
Pid::stepreturns0.0Fondt<=0. But 0 is a valid correction, so the caller cannot distinguish "no correction" from "rejected input." That's a latent hazard in control code.LowPasssilently clamps alpha in the ctor, while Module 03 says "do not silently accept invalid alpha."- Modern fix: return
std::expected<float, ControlError>(C++23, value-based — compatible with exceptions-off) orstd::optional<float>(C++17, if the toolchain's C++23 is uncertain). Teach this as the current idiom for "fail without exceptions."
M4 — Use designated initializers in the lab
The test builds PidGains positionally: {{2.0F, 1.0F, 0.0F, 0.25F, 0.5F}} — five unlabeled floats, easy to transpose. Module 06 even flags that C++20 designated-initializer rules differ from C, then never uses them.
- Show
Pid{{.kp=2.0F, .ki=1.0F, .kd=0.0F, .integral_limit=0.25F, .output_limit=0.5F}}. Readability + transposition-safety, and it closes a loop the course opened.
M5 — Finish the nodiscard story
nodiscard is on LowPass::value() but not on clamp, mix_x, LowPass::update, or Pid::step — all of which return a computed value that must not be dropped. Mark them all.
M6 — Harden the build (cheap, host-only, safe)
- Add
-Wconversion -Wsign-conversion -Wshadow -Wdouble-promotion(I verified the code is already clean under them — so this is free regression insurance;-Wdouble-promotionis especially apt for float-heavy embedded code). - Add an ASan+UBSan host preset (
-fsanitize=address,undefined) — host-only, catches real UB, zero hardware risk; fits the "test the recipe off the stove" metaphor perfectly. - Prefer
target_compile_features(control_core PUBLIC cxx_std_20)over the globalCMAKE_CXX_STANDARDvar (target-scoped is the modern CMake idiom), and emitCMAKE_EXPORT_COMPILE_COMMANDS ONso clang-tidy/clangd work. - Build in Docker (
gcc:14, pinned) rather than on host Apple clang — reproducible toolchain, and it verifiesstd::expected/C++23 on GCC's libstdc++ too. See the modern refactor's Building in Docker section.
M-test — The assert/NDEBUG footgun (fix regardless of the above)
Proven 2026-07-15: compiling the test with -DNDEBUG and a deliberately false assertion still exits 0 — under a release build the entire suite becomes a silent no-op. A course teaching test discipline must not ship a suite that evaporates in release mode.
- Minimal fix: a
CHECKmacro that is independent ofNDEBUG(evaluate, print,return 1on failure). - Better: a single-header host framework (doctest/Catch2) — host-only, no hardware coupling.
- Best, combined with M1: express invariants as
static_assertso they run at compile time and cannot be disabled.
Suggested "Module 2.5 — Modern C++ for constrained targets"
The cleanest way to absorb M1–M6 pedagogically is one new module between Safe Types (02) and Sensor Pipeline (03):
constexpr/noexcept by default · strong types & std::chrono for physical quantities · designated initializers · value-based error handling with std::expected/optional · compile-time tests with static_assert · toolchain-reality caveat: pin the ESP-IDF GCC version and confirm which C++23 features (notably std::expected) it actually ships before relying on them — default the course to C++20 (as it already does) and mark C++23 items "verify toolchain."
One-line verdict
Excellent safety scaffolding and clean code; to earn the "modern C++" label, make the core constexpr/noexcept, put strong types on the dt path it already preaches, adopt designated initializers and std::expected, and replace the NDEBUG-fragile assert harness.