From d1f8ebce14aab564f3b729dfeedbda84034f2a92 Mon Sep 17 00:00:00 2001 From: Simon Gene Gottlieb Date: Mon, 10 Aug 2026 11:11:31 +0200 Subject: [PATCH] feat: Allow decoding for types without default constructor --- CMakeLists.txt | 12 +++++- docs/Tutorial.md | 38 ++++++++++++++++-- include/yaml-cpp/node/impl.h | 71 ++++++++++++++++++++++++++++++--- test/node/node_test.cpp | 76 ++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1eee6ad5..55cc88cd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ cmake_dependent_option(YAML_MSVC_SHARED_RT "CMAKE_SYSTEM_NAME MATCHES Windows" OFF) set(YAML_CPP_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/yaml-cpp" CACHE STRING "Path to install the CMake package to") - + if (YAML_CPP_FORMAT_SOURCE) find_program(YAML_CPP_CLANG_FORMAT_EXE NAMES clang-format) endif() @@ -143,6 +143,16 @@ write_basic_package_version_file( configure_file(yaml-cpp.pc.in yaml-cpp.pc @ONLY) +set(YAML_CPP_USE_OPTIONAL_DEFAULT OFF) +get_target_property(YAML_CPP_CXX_STANDARD yaml-cpp CXX_STANDARD) +if ("${YAML_CPP_CXX_STANDARD}" VERSION_GREATER_EQUAL 17) + set(YAML_CPP_USE_OPTIONAL_DEFAULT ON) +endif() +option(YAML_CPP_USE_OPTIONAL "Support for non-default constructible 'convert` overloads (requires c++17 and newer)" "${YAML_CPP_USE_OPTIONAL_DEFAULT}") +if (YAML_CPP_USE_OPTIONAL) + target_compile_definitions(yaml-cpp PUBLIC YAML_CPP_USE_OPTIONAL) +endif() + if (YAML_CPP_INSTALL) install(TARGETS yaml-cpp EXPORT yaml-cpp-targets diff --git a/docs/Tutorial.md b/docs/Tutorial.md index ec1c7cb31..59aa1dd8c 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -200,6 +200,38 @@ Vec3 v = node["start"].as(); node["end"] = Vec3(2, -1, 0); ``` +## Non-default constructible types (requires c++17 and newer) +Yaml-cpp also supports types that are not default constructible. For this one need to specialize `YAML::convert>`. +Assuming you have: + +```cpp +class Vec3 { + double x, y, z; +public: + Vec3(double x, double y, double z} : x{x}, y{y}, z{z} {} +}; +``` + +you could write (for encoding the previous `convert` with the `encode` method is required) + +```cpp +namespace YAML { +template<> +struct convert> { + static bool decode(const Node& node, std::optional& rhs) { + if(!node.IsSequence() || node.size() != 3) { + return false; + } + rhs.emplace( + node[0].as(), + node[1].as(), + node[2].as() + ); + return true; + } +}; +} + ## Partial specialization If you need to specialize the `convert` struct for a set of types instead of just one you can use partial specialization with the help of `std::enable_if` (SFINAE). @@ -224,7 +256,7 @@ public: node["a"] = a; return node; } - + int a; }; @@ -252,7 +284,7 @@ public: // Implementation of convert::{encode,decode} for all classes derived from or being A namespace YAML { - template + template struct convert::value>::type> { static Node encode(const T &rhs) { Node node = rhs.emit(); @@ -274,4 +306,4 @@ B b = node.as(); b.a = 12; b.b = 42; node = b; -``` \ No newline at end of file +``` diff --git a/include/yaml-cpp/node/impl.h b/include/yaml-cpp/node/impl.h index 4da522fa3..1d72a03c1 100644 --- a/include/yaml-cpp/node/impl.h +++ b/include/yaml-cpp/node/impl.h @@ -15,6 +15,17 @@ #include #include +// Check YAML_CPP_USE_OPTIONAL is set and language standard provides supports +// if available and supported include required header +// otherwise remove YAML_CPP_USE_OPTIONAL definition +#ifdef YAML_CPP_USE_OPTIONAL +#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +#include +#else +#undef YAML_CPP_USE_OPTIONAL +#endif +#endif + namespace YAML { inline Node::Node() : m_isValid(true), m_invalidKey{}, m_pMemory(nullptr), m_pNode(nullptr) {} @@ -94,6 +105,54 @@ inline NodeType::value Node::Type() const { // access // template helpers +#ifdef YAML_CPP_USE_OPTIONAL +template +struct decode_box : std::optional { + decode_box() = default; + template + decode_box(S&&) {} +}; + +template +struct convert> { + static bool decode(const Node& node, decode_box& rhs) { + return convert>::decode(node, rhs); + } +}; +template +struct convert> { + static bool decode(const Node& node, std::optional& rhs) { + if (node.IsNull()) { + return false; + } + rhs.emplace(); + if (!convert::decode(node, *rhs)) { + rhs.reset(); + return false; + } + return true; + } +}; + + +#else + +template +struct decode_box { + T t; + T& operator*() { + return t; + } +}; +template +struct convert> { + static bool decode(const Node& node, decode_box& rhs) { + return convert::decode(node, *rhs); + } +}; + +#endif + template struct as_if { explicit as_if(const Node& node_) : node(node_) {} @@ -103,9 +162,9 @@ struct as_if { if (!node.m_pNode) return fallback; - T t = fallback; - if (convert::decode(node, t)) - return t; + decode_box t{fallback}; + if (convert::decode(node, t)) + return *t; return fallback; } }; @@ -133,9 +192,9 @@ struct as_if { if (!node.m_pNode) // no fallback throw InvalidNode(node.m_invalidKey); - T t; - if (convert::decode(node, t)) - return t; + decode_box t; + if (convert::decode(node, t)) + return *t; throw TypedBadConversion(node.Mark()); } }; diff --git a/test/node/node_test.cpp b/test/node/node_test.cpp index 75ac3db00..6b592600f 100644 --- a/test/node/node_test.cpp +++ b/test/node/node_test.cpp @@ -43,6 +43,23 @@ template > using CustomMap = std::map, class P=std::equal_to> using CustomUnorderedMap = std::unordered_map>>; template , class P=std::equal_to> using CustomUnorderedSet = std::unordered_set>; +struct Vec3 { + double x, y, z; + bool operator==(const Vec3& rhs) const { + return x == rhs.x && y == rhs.y && z == rhs.z; + } +}; + +#ifdef YAML_CPP_USE_OPTIONAL +struct NonDefCtorVec3 { + double x, y, z; + NonDefCtorVec3(double x, double y, double z) : x(x), y(y), z(z) {} + bool operator==(const NonDefCtorVec3& rhs) const { + return x == rhs.x && y == rhs.y && z == rhs.z; + } +}; +#endif + } // anonymous namespace using ::testing::AnyOf; @@ -57,6 +74,43 @@ using ::testing::Eq; } namespace YAML { + +template<> +struct convert { + static Node encode(const Vec3& rhs) { + Node node; + node.push_back(rhs.x); + node.push_back(rhs.y); + node.push_back(rhs.z); + return node; + } + + static bool decode(const Node& node, Vec3& rhs) { + if(!node.IsSequence() || node.size() != 3) { + return false; + } + + rhs.x = node[0].as(); + rhs.y = node[1].as(); + rhs.z = node[2].as(); + return true; + } +}; + +#ifdef YAML_CPP_USE_OPTIONAL +template <> +struct convert> { + static bool decode(const Node& node, std::optional& rhs) { + if (!node.IsSequence() || node.size() != 3) { + return false; + } + rhs.emplace(node[0].as(), node[1].as(), node[2].as()); + + return true; + } +}; +#endif + namespace { TEST(NodeTest, SimpleScalar) { Node node = Node("Hello, World!"); @@ -911,6 +965,28 @@ TEST(NodeTest, CreateMapWithFloatingPoint0Key) { EXPECT_TRUE(node.IsMap()); } +TEST(NodeTest, CustomClassDecoding) { + YAML::Node node; + node.push_back(1.0); + node.push_back(2.0); + node.push_back(3.0); + ASSERT_TRUE(node.IsSequence()); + EXPECT_EQ(node.as(), (Vec3{1.0, 2.0, 3.0})); +} + +TEST(NodeTest, CustomNonDefaultConstructibleClassDecoding) { +#ifdef YAML_CPP_USE_OPTIONAL + YAML::Node node; + node.push_back(1.0); + node.push_back(2.0); + node.push_back(3.0); + ASSERT_TRUE(node.IsSequence()); + EXPECT_EQ(node.as(), (NonDefCtorVec3{1.0, 2.0, 3.0})); +#else + GTEST_SKIP() << "Compile with C++17 for customizing non-default-constructible custom types."; +#endif +} + class NodeEmitterTest : public ::testing::Test { protected: void ExpectOutput(const std::string& output, const Node& node) {