From a4029ce684816b9eb7c503bc10c6d8713a521800 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:29:52 +0000 Subject: [PATCH 1/6] Add Fw::Optional type for C++14 environments Incorporates an Optional class into F Prime core, providing a type-safe container for optional values in C++14 environments where std::optional is unavailable. Based on the implementation from fprime-vorago (Va416x0/Types/Optional.hpp), adapted to use the Fw namespace and F Prime conventions. Resolves nasa/fprime#4986 Co-Authored-By: michael.d.starch --- Fw/Types/CMakeLists.txt | 8 ++ Fw/Types/Optional.hpp | 170 ++++++++++++++++++++++++++++++ Fw/Types/test/ut/OptionalTest.cpp | 168 +++++++++++++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 Fw/Types/Optional.hpp create mode 100644 Fw/Types/test/ut/OptionalTest.cpp diff --git a/Fw/Types/CMakeLists.txt b/Fw/Types/CMakeLists.txt index 3698ad4edc6..6e3e2876cac 100644 --- a/Fw/Types/CMakeLists.txt +++ b/Fw/Types/CMakeLists.txt @@ -89,5 +89,13 @@ register_fprime_ut( Fw_StringScan_sscanf ) +register_fprime_ut( + Fw_Types_Optional_ut_exe + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/OptionalTest.cpp" + DEPENDS + Fw_Types +) + # Non-test directory add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/GTest") diff --git a/Fw/Types/Optional.hpp b/Fw/Types/Optional.hpp new file mode 100644 index 00000000000..4d96c90d422 --- /dev/null +++ b/Fw/Types/Optional.hpp @@ -0,0 +1,170 @@ +// ====================================================================== +// \title Optional.hpp +// \author celskeggs +// \brief hpp file for Optional type +// +// \description +// A lightweight optional type for C++14 environments where +// std::optional (C++17) is not available. +// +// \copyright +// Copyright (C) 2025-2026 California Institute of Technology. +// ALL RIGHTS RESERVED. United States Government Sponsorship +// acknowledged. +// +// ====================================================================== + +#ifndef Fw_Optional_HPP +#define Fw_Optional_HPP + +#include + +namespace Fw { + +//! \brief Sentinel type representing the absence of a value in an Optional +struct Absent { + constexpr explicit Absent() {} +}; + +//! \brief Global constant representing an absent/empty optional value +constexpr Absent ABSENT = Absent(); + +//! \class Optional +//! \brief A type-safe container for an optional value +//! +//! Provides a C++14-compatible alternative to std::optional. +//! Only supports types with trivial destructors (no heap-allocated resources). +//! +//! \tparam T The type of the contained value. Must be trivially destructible. +//! +template +class Optional { + union { + char m_dummy; + T m_val; + }; + bool m_engaged; + + public: + // ---------------------------------------------------------------------- + // Construction + // ---------------------------------------------------------------------- + + //! Construct an empty Optional + constexpr Optional() : m_dummy(0), m_engaged(false) {} + + //! Construct an empty Optional from ABSENT sentinel + constexpr Optional(const Absent& a) : Optional() { (void)a; } + + //! Construct an Optional containing a value + constexpr Optional(const T& t) : m_val(t), m_engaged(true) {} + + //! Copy constructor + constexpr Optional(const Optional& other) : Optional() { + if (other.has_value()) { + *this = other.value(); + } + } + + //! Destructor (trivial — T must have a trivial destructor) + ~Optional() = default; + + // ---------------------------------------------------------------------- + // Observers + // ---------------------------------------------------------------------- + + //! Check whether the Optional contains a value + //! \return true if a value is present + bool has_value() const { return m_engaged; } + + //! Access the contained value + //! \pre has_value() must be true + //! \return Mutable reference to the contained value + T& value() { + FW_ASSERT(m_engaged); + return m_val; + } + + //! Access the contained value (const) + //! \pre has_value() must be true + //! \return Const reference to the contained value + const T& value() const { + FW_ASSERT(m_engaged); + return m_val; + } + + //! Access the value or return a default + //! \return The contained value if present, otherwise the provided default + const T& value_or(const T& default_value) const { + return m_engaged ? m_val : default_value; + } + + // ---------------------------------------------------------------------- + // Modifiers + // ---------------------------------------------------------------------- + + //! Reset the Optional to an empty state + void reset() { m_engaged = false; } + + // ---------------------------------------------------------------------- + // Assignment operators + // ---------------------------------------------------------------------- + + //! Assign a value into the Optional + Optional& operator=(const T& t) { + m_engaged = true; + m_val = t; + return *this; + } + + //! Reset the Optional via ABSENT assignment + Optional& operator=(const Absent& a) { + (void)a; + reset(); + return *this; + } + + //! Copy assignment operator + Optional& operator=(const Optional& other) { + if (other.has_value()) { + *this = other.value(); + } else { + reset(); + } + return *this; + } + + // ---------------------------------------------------------------------- + // Comparison operators + // ---------------------------------------------------------------------- + + //! Compare with ABSENT sentinel + bool operator==(const Absent& a) const { + (void)a; + return !m_engaged; + } + + //! Compare with ABSENT sentinel (inequality) + bool operator!=(const Absent& a) const { + (void)a; + return m_engaged; + } + + //! Equality comparison with another Optional + bool operator==(const Optional& other) const { + if (m_engaged != other.m_engaged) { + return false; + } + if (!m_engaged) { + return true; + } + return m_val == other.m_val; + } + + //! Inequality comparison with another Optional + bool operator!=(const Optional& other) const { return !(*this == other); } +}; + +} // namespace Fw + +#endif diff --git a/Fw/Types/test/ut/OptionalTest.cpp b/Fw/Types/test/ut/OptionalTest.cpp new file mode 100644 index 00000000000..86c0dc404c6 --- /dev/null +++ b/Fw/Types/test/ut/OptionalTest.cpp @@ -0,0 +1,168 @@ +#include +#include + +// Test with a simple integer type +TEST(OptionalTest, DefaultConstructIsEmpty) { + Fw::Optional opt; + ASSERT_FALSE(opt.has_value()); + ASSERT_EQ(opt, Fw::ABSENT); +} + +TEST(OptionalTest, AbsentConstructIsEmpty) { + Fw::Optional opt(Fw::ABSENT); + ASSERT_FALSE(opt.has_value()); +} + +TEST(OptionalTest, ValueConstructHasValue) { + Fw::Optional opt(42); + ASSERT_TRUE(opt.has_value()); + ASSERT_EQ(opt.value(), 42); +} + +TEST(OptionalTest, CopyConstructEmpty) { + Fw::Optional opt1; + Fw::Optional opt2(opt1); + ASSERT_FALSE(opt2.has_value()); +} + +TEST(OptionalTest, CopyConstructWithValue) { + Fw::Optional opt1(99); + Fw::Optional opt2(opt1); + ASSERT_TRUE(opt2.has_value()); + ASSERT_EQ(opt2.value(), 99); +} + +TEST(OptionalTest, AssignValue) { + Fw::Optional opt; + opt = 7; + ASSERT_TRUE(opt.has_value()); + ASSERT_EQ(opt.value(), 7); +} + +TEST(OptionalTest, AssignAbsent) { + Fw::Optional opt(10); + opt = Fw::ABSENT; + ASSERT_FALSE(opt.has_value()); +} + +TEST(OptionalTest, Reset) { + Fw::Optional opt(5); + ASSERT_TRUE(opt.has_value()); + opt.reset(); + ASSERT_FALSE(opt.has_value()); +} + +TEST(OptionalTest, ValueOrWithValue) { + Fw::Optional opt(42); + ASSERT_EQ(opt.value_or(0), 42); +} + +TEST(OptionalTest, ValueOrWithoutValue) { + Fw::Optional opt; + ASSERT_EQ(opt.value_or(99), 99); +} + +TEST(OptionalTest, MutableValueAccess) { + Fw::Optional opt(10); + opt.value() = 20; + ASSERT_EQ(opt.value(), 20); +} + +TEST(OptionalTest, CopyAssignmentWithValue) { + Fw::Optional opt1(42); + Fw::Optional opt2; + opt2 = opt1; + ASSERT_TRUE(opt2.has_value()); + ASSERT_EQ(opt2.value(), 42); +} + +TEST(OptionalTest, CopyAssignmentWithEmpty) { + Fw::Optional opt1; + Fw::Optional opt2(42); + opt2 = opt1; + ASSERT_FALSE(opt2.has_value()); +} + +TEST(OptionalTest, EqualityBothEmpty) { + Fw::Optional opt1; + Fw::Optional opt2; + ASSERT_EQ(opt1, opt2); +} + +TEST(OptionalTest, EqualitySameValue) { + Fw::Optional opt1(42); + Fw::Optional opt2(42); + ASSERT_EQ(opt1, opt2); +} + +TEST(OptionalTest, InequalityDifferentValues) { + Fw::Optional opt1(1); + Fw::Optional opt2(2); + ASSERT_NE(opt1, opt2); +} + +TEST(OptionalTest, InequalityOneEmpty) { + Fw::Optional opt1(1); + Fw::Optional opt2; + ASSERT_NE(opt1, opt2); +} + +TEST(OptionalTest, AbsentComparisonOperators) { + Fw::Optional empty; + Fw::Optional full(42); + ASSERT_TRUE(empty == Fw::ABSENT); + ASSERT_FALSE(empty != Fw::ABSENT); + ASSERT_FALSE(full == Fw::ABSENT); + ASSERT_TRUE(full != Fw::ABSENT); +} + +TEST(OptionalTest, ReassignValue) { + Fw::Optional opt(1); + opt = 2; + ASSERT_EQ(opt.value(), 2); + opt = 3; + ASSERT_EQ(opt.value(), 3); +} + +// Test with a struct type +struct TestStruct { + I32 x; + I32 y; + bool operator==(const TestStruct& other) const { return x == other.x && y == other.y; } +}; + +TEST(OptionalTest, StructValue) { + TestStruct s{10, 20}; + Fw::Optional opt(s); + ASSERT_TRUE(opt.has_value()); + ASSERT_EQ(opt.value().x, 10); + ASSERT_EQ(opt.value().y, 20); +} + +TEST(OptionalTest, StructAssign) { + Fw::Optional opt; + TestStruct s{5, 6}; + opt = s; + ASSERT_TRUE(opt.has_value()); + ASSERT_EQ(opt.value().x, 5); + ASSERT_EQ(opt.value().y, 6); +} + +// Test with a floating point type +TEST(OptionalTest, FloatingPointValue) { + Fw::Optional opt(3.14); + ASSERT_TRUE(opt.has_value()); + ASSERT_DOUBLE_EQ(opt.value(), 3.14); +} + +// Test constexpr construction +TEST(OptionalTest, ConstexprEmpty) { + constexpr Fw::Optional opt; + ASSERT_FALSE(opt.has_value()); +} + +TEST(OptionalTest, ConstexprWithValue) { + constexpr Fw::Optional opt(42); + ASSERT_TRUE(opt.has_value()); + ASSERT_EQ(opt.value(), 42); +} From b5a73623439bc5855bb6719614d7733a7536fed9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:34:28 +0000 Subject: [PATCH 2/6] Add 'celskeggs' to spell checker expected words list Co-Authored-By: michael.d.starch --- .github/actions/spelling/expect.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 2a4f9ac5375..f3182e328e1 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -87,6 +87,7 @@ CDR cdh CDHCORE CDHCORESUBTOPOLOGY +celskeggs cerrno CFDP cff From 04800fc61f026baab0c87c5f44b8a9c577bdd13e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:40:13 +0000 Subject: [PATCH 3/6] Add static_assert for trivially copyable types in Optional Restricts Fw::Optional to trivially copyable T via static_assert. This makes the union-based storage well-defined: writing to any member of a union of trivially-copyable types is safe regardless of which member was last active. Co-Authored-By: michael.d.starch --- Fw/Types/Optional.hpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Fw/Types/Optional.hpp b/Fw/Types/Optional.hpp index 4d96c90d422..34cfcc0175b 100644 --- a/Fw/Types/Optional.hpp +++ b/Fw/Types/Optional.hpp @@ -18,6 +18,7 @@ #define Fw_Optional_HPP #include +#include namespace Fw { @@ -33,12 +34,16 @@ constexpr Absent ABSENT = Absent(); //! \brief A type-safe container for an optional value //! //! Provides a C++14-compatible alternative to std::optional. -//! Only supports types with trivial destructors (no heap-allocated resources). +//! Only supports trivially copyable types (no non-trivial constructors, +//! destructors, or copy/move operators). //! -//! \tparam T The type of the contained value. Must be trivially destructible. +//! \tparam T The type of the contained value. Must be trivially copyable. //! template class Optional { + static_assert(std::is_trivially_copyable::value, + "Fw::Optional only supports trivially copyable types"); + union { char m_dummy; T m_val; @@ -66,7 +71,7 @@ class Optional { } } - //! Destructor (trivial — T must have a trivial destructor) + //! Destructor (trivial — T must be trivially copyable) ~Optional() = default; // ---------------------------------------------------------------------- From 3e5a2cd8bc834917cba6e84ddad49ae897f868b8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:42:18 +0000 Subject: [PATCH 4/6] Apply clang-format to Optional.hpp and OptionalTest.cpp Co-Authored-By: michael.d.starch --- Fw/Types/Optional.hpp | 7 ++----- Fw/Types/test/ut/OptionalTest.cpp | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Fw/Types/Optional.hpp b/Fw/Types/Optional.hpp index 34cfcc0175b..9481571e818 100644 --- a/Fw/Types/Optional.hpp +++ b/Fw/Types/Optional.hpp @@ -41,8 +41,7 @@ constexpr Absent ABSENT = Absent(); //! template class Optional { - static_assert(std::is_trivially_copyable::value, - "Fw::Optional only supports trivially copyable types"); + static_assert(std::is_trivially_copyable::value, "Fw::Optional only supports trivially copyable types"); union { char m_dummy; @@ -100,9 +99,7 @@ class Optional { //! Access the value or return a default //! \return The contained value if present, otherwise the provided default - const T& value_or(const T& default_value) const { - return m_engaged ? m_val : default_value; - } + const T& value_or(const T& default_value) const { return m_engaged ? m_val : default_value; } // ---------------------------------------------------------------------- // Modifiers diff --git a/Fw/Types/test/ut/OptionalTest.cpp b/Fw/Types/test/ut/OptionalTest.cpp index 86c0dc404c6..35f1f12328b 100644 --- a/Fw/Types/test/ut/OptionalTest.cpp +++ b/Fw/Types/test/ut/OptionalTest.cpp @@ -1,5 +1,5 @@ -#include #include +#include // Test with a simple integer type TEST(OptionalTest, DefaultConstructIsEmpty) { From f0a87f83d21e5c925c21cda3310d104905eade03 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:44:34 +0000 Subject: [PATCH 5/6] Add 'copyable' to spell checker expected words list Co-Authored-By: michael.d.starch --- .github/actions/spelling/expect.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index f3182e328e1..265459dc448 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -135,6 +135,7 @@ COUNTINGSEMAPHORE countingsemaphore cookiecutter cooldown +copyable coravy coreutils Coverity From 7f45c665a41f26a89c3f502059b2d91393077f49 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:51:13 +0000 Subject: [PATCH 6/6] Fix constexpr copy ctor and value_or return type - Remove constexpr from copy constructor: it calls non-constexpr members and cannot be evaluated at compile time in C++14. - Return T by value from value_or() instead of const T&: since T is constrained to trivially copyable types this is equally efficient and avoids dangling references when a temporary is passed. Co-Authored-By: michael.d.starch --- Fw/Types/Optional.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Fw/Types/Optional.hpp b/Fw/Types/Optional.hpp index 9481571e818..c77bc34ef3f 100644 --- a/Fw/Types/Optional.hpp +++ b/Fw/Types/Optional.hpp @@ -64,7 +64,7 @@ class Optional { constexpr Optional(const T& t) : m_val(t), m_engaged(true) {} //! Copy constructor - constexpr Optional(const Optional& other) : Optional() { + Optional(const Optional& other) : Optional() { if (other.has_value()) { *this = other.value(); } @@ -99,7 +99,7 @@ class Optional { //! Access the value or return a default //! \return The contained value if present, otherwise the provided default - const T& value_or(const T& default_value) const { return m_engaged ? m_val : default_value; } + T value_or(const T& default_value) const { return m_engaged ? m_val : default_value; } // ---------------------------------------------------------------------- // Modifiers