Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
38 changes: 35 additions & 3 deletions docs/Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,38 @@ Vec3 v = node["start"].as<Vec3>();
node["end"] = Vec3(2, -1, 0);
```

## Non-default constructible types (requires c++17 and newer)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c++17 and newer -> C++17 or newer

Yaml-cpp also supports types that are not default constructible. For this one need to specialize `YAML::convert<std::optional<>>`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default constructible -> default-constructible

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this one need to specialize -> For this, one needs to specialize

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<Vec3>` with the `encode` method is required)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for encoding the previous -> for encoding, the previous


```cpp
namespace YAML {
template<>
struct convert<std::optional<Vec3>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd doubt that std::optional is a great choice here, std::expected seems to be more appropriate, as it enables users to provide informative failure data more easily than e.g. via exception handling. It's been available in mainstream toolchains for 3-4 years now.

static bool decode(const Node& node, std::optional<Vec3>& rhs) {

@alex-thiessen-for-siemens alex-thiessen-for-siemens Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this interface now has two bools (one return value, one std::optional-embedded), what's their combinatory semantics?

if(!node.IsSequence() || node.size() != 3) {
return false;
}
rhs.emplace(
node[0].as<double>(),
node[1].as<double>(),
node[2].as<double>()
);
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).
Expand All @@ -224,7 +256,7 @@ public:
node["a"] = a;
return node;
}

int a;
};

Expand Down Expand Up @@ -252,7 +284,7 @@ public:

// Implementation of convert::{encode,decode} for all classes derived from or being A
namespace YAML {
template<typename T>
template<typename T>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to clean all whitespace in a separate effort, adding a static check for that?

struct convert<T, typename std::enable_if<std::is_base_of<A, T>::value>::type> {
static Node encode(const T &rhs) {
Node node = rhs.emit();
Expand All @@ -274,4 +306,4 @@ B b = node.as<B>();
b.a = 12;
b.b = 42;
node = b;
```
```
71 changes: 65 additions & 6 deletions include/yaml-cpp/node/impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
#include <sstream>
#include <string>

// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a weird check, maybe C++20 should be the base line so that more straightforward feature-test macros could be used instead.

#include <optional>
#else
#undef YAML_CPP_USE_OPTIONAL
#endif
#endif

namespace YAML {
inline Node::Node()
: m_isValid(true), m_invalidKey{}, m_pMemory(nullptr), m_pNode(nullptr) {}
Expand Down Expand Up @@ -94,6 +105,54 @@ inline NodeType::value Node::Type() const {
// access

// template helpers
#ifdef YAML_CPP_USE_OPTIONAL
template <typename T>
struct decode_box : std::optional<T> {
decode_box() = default;
template <typename S>
decode_box(S&&) {}
};

template <typename T>
struct convert<decode_box<T>> {
static bool decode(const Node& node, decode_box<T>& rhs) {
return convert<std::optional<T>>::decode(node, rhs);
}
};
template <typename T>
struct convert<std::optional<T>> {
static bool decode(const Node& node, std::optional<T>& rhs) {
if (node.IsNull()) {
return false;
}
rhs.emplace();
if (!convert<T>::decode(node, *rhs)) {
rhs.reset();
return false;
}
return true;
}
};


#else

template <typename T>
struct decode_box {
T t;
T& operator*() {
return t;
}
};
template <typename T>
struct convert<decode_box<T>> {
static bool decode(const Node& node, decode_box<T>& rhs) {
return convert<T>::decode(node, *rhs);
}
};

#endif

template <typename T, typename S>
struct as_if {
explicit as_if(const Node& node_) : node(node_) {}
Expand All @@ -103,9 +162,9 @@ struct as_if {
if (!node.m_pNode)
return fallback;

T t = fallback;
if (convert<T>::decode(node, t))
return t;
decode_box<T> t{fallback};
if (convert<decltype(t)>::decode(node, t))
return *t;
return fallback;
}
};
Expand Down Expand Up @@ -133,9 +192,9 @@ struct as_if<T, void> {
if (!node.m_pNode) // no fallback
throw InvalidNode(node.m_invalidKey);

T t;
if (convert<T>::decode(node, t))
return t;
decode_box<T> t;
if (convert<decltype(t)>::decode(node, t))
return *t;
throw TypedBadConversion<T>(node.Mark());
}
};
Expand Down
76 changes: 76 additions & 0 deletions test/node/node_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ template <class K, class V, class C=std::less<K>> using CustomMap = std::map<K,V
template <class K, class V, class H=std::hash<K>, class P=std::equal_to<K>> using CustomUnorderedMap = std::unordered_map<K,V,H,P,CustomAllocator<std::pair<const K,V>>>;
template <class K, class H=std::hash<K>, class P=std::equal_to<K>> using CustomUnorderedSet = std::unordered_set<K,H,P,CustomAllocator<K>>;

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;
Expand All @@ -57,6 +74,43 @@ using ::testing::Eq;
}

namespace YAML {

template<>
struct convert<Vec3> {
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<double>();
rhs.y = node[1].as<double>();
rhs.z = node[2].as<double>();
return true;
}
};

#ifdef YAML_CPP_USE_OPTIONAL
template <>
struct convert<std::optional<NonDefCtorVec3>> {
static bool decode(const Node& node, std::optional<NonDefCtorVec3>& rhs) {
if (!node.IsSequence() || node.size() != 3) {
return false;
}
rhs.emplace(node[0].as<double>(), node[1].as<double>(), node[2].as<double>());

return true;
}
};
#endif

namespace {
TEST(NodeTest, SimpleScalar) {
Node node = Node("Hello, World!");
Expand Down Expand Up @@ -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>(), (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>(), (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) {
Expand Down