From ac92c7dbcc42785451ee829532c24817bf7ac144 Mon Sep 17 00:00:00 2001 From: Leonardo Arcari Date: Wed, 18 Feb 2026 16:56:55 +0100 Subject: [PATCH] feat(cpp): generate safe getters for optional fields --- .../sbe/generation/cpp/CppGenerator.java | 51 ++++++ sbe-tool/src/test/cpp/CMakeLists.txt | 9 + .../test/cpp/OptionalAccessorsCodeGenTest.cpp | 155 ++++++++++++++++++ .../resources/optional-accessors-schema.xml | 43 +++++ 4 files changed, 258 insertions(+) create mode 100644 sbe-tool/src/test/cpp/OptionalAccessorsCodeGenTest.cpp create mode 100644 sbe-tool/src/test/resources/optional-accessors-schema.xml diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java index 3ee0208cbb..626f15de3a 100755 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/cpp/CppGenerator.java @@ -1733,6 +1733,7 @@ private CharSequence generateFileHeader( "#if __cplusplus >= 201703L\n" + "# include \n" + + "# include \n" + "# define SBE_NODISCARD [[nodiscard]]\n" + "# if !defined(SBE_USE_STRING_VIEW)\n" + "# define SBE_USE_STRING_VIEW 1\n" + @@ -1753,6 +1754,7 @@ private CharSequence generateFileHeader( "#endif\n\n" + "#include \n" + + "#include \n" + "#include \n" + "#include \n" + "#include \n" + @@ -2124,6 +2126,16 @@ private static String noexceptDeclaration(final FieldPrecedenceModel fieldPreced return fieldPrecedenceModel == null ? " SBE_NOEXCEPT" : ""; } + private static String optionalPrimitiveNullCheckExpression(final PrimitiveType primitiveType) + { + if (primitiveType == PrimitiveType.FLOAT || primitiveType == PrimitiveType.DOUBLE) + { + return "std::isnan(value) || value == nullValue"; + } + + return "value == nullValue"; + } + private void generateSingleValueProperty( final StringBuilder sb, final String containingClassName, @@ -2156,6 +2168,27 @@ private void generateSingleValueProperty( generateLoadValue(primitiveType, Integer.toString(offset), encodingToken.encoding().byteOrder(), indent), noexceptDeclaration); + if (propertyToken.isOptionalEncoding()) + { + new Formatter(sb).format("\n" + + indent + " #if __cplusplus >= 201703L\n" + + indent + " SBE_NODISCARD std::optional<%1$s> %2$sOpt() const%4$s\n" + + indent + " {\n" + + indent + " const %1$s value = %2$s();\n" + + indent + " const %1$s nullValue = %2$sNullValue();\n" + + indent + " if (%3$s)\n" + + indent + " {\n" + + indent + " return std::nullopt;\n" + + indent + " }\n\n" + + indent + " return value;\n" + + indent + " }\n" + + indent + " #endif\n", + cppTypeName, + propertyName, + optionalPrimitiveNullCheckExpression(primitiveType), + noexceptDeclaration); + } + final CharSequence storeValue = generateStoreValue( primitiveType, "", Integer.toString(offset), encodingToken.encoding().byteOrder(), indent); @@ -3613,6 +3646,24 @@ private void generateEnumProperty( offset, accessOrderListenerCall); + if (fieldToken.isOptionalEncoding()) + { + new Formatter(sb).format("\n" + + indent + " #if __cplusplus >= 201703L\n" + + indent + " SBE_NODISCARD std::optional<%1$s::Value> %2$sOpt() const\n" + + indent + " {\n" + + indent + " const %1$s::Value value = %2$s();\n" + + indent + " if (%1$s::NULL_VALUE == value)\n" + + indent + " {\n" + + indent + " return std::nullopt;\n" + + indent + " }\n\n" + + indent + " return value;\n" + + indent + " }\n" + + indent + " #endif\n", + enumName, + propertyName); + } + new Formatter(sb).format("\n" + indent + " %1$s &%2$s(const %3$s::Value value)%8$s\n" + indent + " {\n" + diff --git a/sbe-tool/src/test/cpp/CMakeLists.txt b/sbe-tool/src/test/cpp/CMakeLists.txt index 0137b6de35..99513cc095 100644 --- a/sbe-tool/src/test/cpp/CMakeLists.txt +++ b/sbe-tool/src/test/cpp/CMakeLists.txt @@ -43,6 +43,7 @@ set(ISSUE889_SCHEMA ${CODEC_SCHEMA_DIR}/issue889.xml) set(ACCESS_ORDER_SCHEMA ${CODEC_SCHEMA_DIR}/field-order-check-schema.xml) set(VERSIONED_MESSAGE_V1_SCHEMA ${CODEC_SCHEMA_DIR}/versioned-message-v1.xml) set(VERSIONED_MESSAGE_V2_SCHEMA ${CODEC_SCHEMA_DIR}/versioned-message-v2.xml) +set(OPTIONAL_ACCESSORS_SCHEMA ${CODEC_SCHEMA_DIR}/optional-accessors-schema.xml) set(GENERATED_CODECS ${CXX_CODEC_TARGET_DIR} @@ -61,6 +62,7 @@ add_custom_command( ${ACCESS_ORDER_SCHEMA} ${VERSIONED_MESSAGE_V1_SCHEMA} ${VERSIONED_MESSAGE_V2_SCHEMA} + ${OPTIONAL_ACCESSORS_SCHEMA} sbe-jar ${SBE_JAR} COMMAND ${Java_JAVA_EXECUTABLE} --add-opens java.base/jdk.internal.misc=ALL-UNNAMED @@ -84,6 +86,7 @@ add_custom_command( ${ACCESS_ORDER_SCHEMA} ${VERSIONED_MESSAGE_V1_SCHEMA} ${VERSIONED_MESSAGE_V2_SCHEMA} + ${OPTIONAL_ACCESSORS_SCHEMA} ) add_custom_target(codecs DEPENDS ${GENERATED_CODECS}) @@ -108,6 +111,8 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "7.0") sbe_test(DtoTest codecs) target_compile_features(DtoTest PRIVATE cxx_std_17) + sbe_test(OptionalAccessorsCodeGenTest codecs) + target_compile_features(OptionalAccessorsCodeGenTest PRIVATE cxx_std_17) endif() # Check if the GCC version supports C++20 @@ -121,6 +126,8 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "CLang") if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "4.0") sbe_test(DtoTest codecs) target_compile_features(DtoTest PRIVATE cxx_std_17) + sbe_test(OptionalAccessorsCodeGenTest codecs) + target_compile_features(OptionalAccessorsCodeGenTest PRIVATE cxx_std_17) endif() # Check if CLang version supports C++20 @@ -133,7 +140,9 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") # Check if MSVC version supports C++17 / C++20 if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "19.14") sbe_test(DtoTest codecs) + sbe_test(OptionalAccessorsCodeGenTest codecs) target_compile_options(DtoTest PRIVATE /std:c++17) + target_compile_options(OptionalAccessorsCodeGenTest PRIVATE /std:c++17) target_compile_options(CodeGenTest PRIVATE /std:c++20) endif() endif() diff --git a/sbe-tool/src/test/cpp/OptionalAccessorsCodeGenTest.cpp b/sbe-tool/src/test/cpp/OptionalAccessorsCodeGenTest.cpp new file mode 100644 index 0000000000..9e5ba24ab5 --- /dev/null +++ b/sbe-tool/src/test/cpp/OptionalAccessorsCodeGenTest.cpp @@ -0,0 +1,155 @@ +/* + * Copyright 2013-2025 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +#include "gtest/gtest.h" +#include "optional_accessors_test/OptionalAccessorsMessage.h" + +using namespace optional::accessors::test; + +class OptionalAccessorsCodeGenTest : public testing::Test +{ +public: + char m_buffer[2048] = {}; + OptionalAccessorsMessage m_msg = {}; + OptionalAccessorsMessage m_msgDecoder = {}; +}; + +TEST_F(OptionalAccessorsCodeGenTest, shouldReturnEmptyOptionalsForNullSentinels) +{ + m_msg.wrapForEncode(m_buffer, 0, sizeof(m_buffer)) + .rootPrimitive(OptionalAccessorsMessage::rootPrimitiveNullValue()) + .rootFloat(OptionalAccessorsMessage::rootFloatNullValue()) + .rootEnum(OptionalEnum::NULL_VALUE); + + OptionalComposite &composite = m_msg.rootComposite(); + composite + .compositePrimitive(OptionalComposite::compositePrimitiveNullValue()) + .compositeFloat(OptionalComposite::compositeFloatNullValue()) + .compositeEnum(CompositeEnum::NULL_VALUE); + + OptionalAccessorsMessage::Entries &entries = m_msg.entriesCount(1); + entries.next() + .groupPrimitive(OptionalAccessorsMessage::Entries::groupPrimitiveNullValue()) + .groupEnum(OptionalEnum::NULL_VALUE); + + const std::uint64_t encodedLength = m_msg.encodedLength(); + + m_msgDecoder.wrapForDecode( + m_buffer, + 0, + OptionalAccessorsMessage::sbeBlockLength(), + OptionalAccessorsMessage::sbeSchemaVersion(), + encodedLength); + + // Expect: raw accessors still return null values + EXPECT_EQ(m_msgDecoder.rootPrimitive(), OptionalAccessorsMessage::rootPrimitiveNullValue()); + EXPECT_TRUE(std::isnan(m_msgDecoder.rootFloat())); + EXPECT_EQ(m_msgDecoder.rootEnum(), OptionalEnum::NULL_VALUE); + // Expect: optional accessors return empty optionals + EXPECT_FALSE(m_msgDecoder.rootPrimitiveOpt().has_value()); + EXPECT_FALSE(m_msgDecoder.rootFloatOpt().has_value()); + EXPECT_FALSE(m_msgDecoder.rootEnumOpt().has_value()); + + // Expect: optional accessors generated for composite types + OptionalComposite &compositeDecoder = m_msgDecoder.rootComposite(); + EXPECT_EQ(compositeDecoder.compositePrimitive(), OptionalComposite::compositePrimitiveNullValue()); + EXPECT_TRUE(std::isnan(compositeDecoder.compositeFloat())); + EXPECT_EQ(compositeDecoder.compositeEnum(), CompositeEnum::NULL_VALUE); + EXPECT_FALSE(compositeDecoder.compositePrimitiveOpt().has_value()); + EXPECT_FALSE(compositeDecoder.compositeFloatOpt().has_value()); + + // Expect: optional accessors generated for group fields + OptionalAccessorsMessage::Entries &entriesDecoder = m_msgDecoder.entries(); + EXPECT_EQ(entriesDecoder.count(), 1u); + ASSERT_TRUE(entriesDecoder.hasNext()); + entriesDecoder.next(); + EXPECT_EQ(entriesDecoder.groupPrimitive(), OptionalAccessorsMessage::Entries::groupPrimitiveNullValue()); + EXPECT_EQ(entriesDecoder.groupEnum(), OptionalEnum::NULL_VALUE); + EXPECT_FALSE(entriesDecoder.groupPrimitiveOpt().has_value()); + EXPECT_FALSE(entriesDecoder.groupEnumOpt().has_value()); +} + +TEST_F(OptionalAccessorsCodeGenTest, shouldReturnValueOptionalsForNonNullFields) +{ + static const std::int32_t ROOT_PRIMITIVE = 42; + static const float ROOT_FLOAT = 101.25f; + static const std::int32_t COMPOSITE_PRIMITIVE = 7; + static const float COMPOSITE_FLOAT = 3.5f; + static const std::uint16_t GROUP_PRIMITIVE = 65000; + + m_msg.wrapForEncode(m_buffer, 0, sizeof(m_buffer)) + .rootPrimitive(ROOT_PRIMITIVE) + .rootFloat(ROOT_FLOAT) + .rootEnum(OptionalEnum::A); + + OptionalComposite &composite = m_msg.rootComposite(); + composite + .compositePrimitive(COMPOSITE_PRIMITIVE) + .compositeFloat(COMPOSITE_FLOAT) + .compositeEnum(CompositeEnum::Y); + + OptionalAccessorsMessage::Entries &entries = m_msg.entriesCount(1); + entries.next() + .groupPrimitive(GROUP_PRIMITIVE) + .groupEnum(OptionalEnum::A); + + const std::uint64_t encodedLength = m_msg.encodedLength(); + + m_msgDecoder.wrapForDecode( + m_buffer, + 0, + OptionalAccessorsMessage::sbeBlockLength(), + OptionalAccessorsMessage::sbeSchemaVersion(), + encodedLength); + + const std::optional rootPrimitiveOpt = m_msgDecoder.rootPrimitiveOpt(); + ASSERT_TRUE(rootPrimitiveOpt.has_value()); + EXPECT_EQ(rootPrimitiveOpt.value(), ROOT_PRIMITIVE); + + const std::optional rootFloatOpt = m_msgDecoder.rootFloatOpt(); + ASSERT_TRUE(rootFloatOpt.has_value()); + EXPECT_FLOAT_EQ(rootFloatOpt.value(), ROOT_FLOAT); + + const std::optional rootEnumOpt = m_msgDecoder.rootEnumOpt(); + ASSERT_TRUE(rootEnumOpt.has_value()); + EXPECT_EQ(rootEnumOpt.value(), OptionalEnum::A); + + OptionalComposite &compositeDecoder = m_msgDecoder.rootComposite(); + + const std::optional compositePrimitiveOpt = compositeDecoder.compositePrimitiveOpt(); + ASSERT_TRUE(compositePrimitiveOpt.has_value()); + EXPECT_EQ(compositePrimitiveOpt.value(), COMPOSITE_PRIMITIVE); + + const std::optional compositeFloatOpt = compositeDecoder.compositeFloatOpt(); + ASSERT_TRUE(compositeFloatOpt.has_value()); + EXPECT_FLOAT_EQ(compositeFloatOpt.value(), COMPOSITE_FLOAT); + EXPECT_EQ(compositeDecoder.compositeEnum(), CompositeEnum::Y); + + OptionalAccessorsMessage::Entries &entriesDecoder = m_msgDecoder.entries(); + EXPECT_EQ(entriesDecoder.count(), 1u); + ASSERT_TRUE(entriesDecoder.hasNext()); + entriesDecoder.next(); + + const std::optional groupPrimitiveOpt = entriesDecoder.groupPrimitiveOpt(); + ASSERT_TRUE(groupPrimitiveOpt.has_value()); + EXPECT_EQ(groupPrimitiveOpt.value(), GROUP_PRIMITIVE); + + const std::optional groupEnumOpt = entriesDecoder.groupEnumOpt(); + ASSERT_TRUE(groupEnumOpt.has_value()); + EXPECT_EQ(groupEnumOpt.value(), OptionalEnum::A); +} diff --git a/sbe-tool/src/test/resources/optional-accessors-schema.xml b/sbe-tool/src/test/resources/optional-accessors-schema.xml new file mode 100644 index 0000000000..bc1deec828 --- /dev/null +++ b/sbe-tool/src/test/resources/optional-accessors-schema.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + 1 + 2 + + + + + + 10 + 20 + + + + + + + + + + + + + +