Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seamlessly support non-default-constructable custom class deserialization #1087

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
Next Next commit
a working formulation integrating a new API for non-default construct…
…able custom types, while seamlessly supporting the old API; see Tutorial.mp for details
marcel committed Mar 3, 2022
commit b46d569f8cc4e634c469970ec2c2037071633215
49 changes: 47 additions & 2 deletions docs/Tutorial.md
Original file line number Diff line number Diff line change
@@ -198,5 +198,50 @@ Then you could use `Vec3` wherever you could use any other type:
```cpp
YAML::Node node = YAML::Load("start: [1, 3, 0]");
Vec3 v = node["start"].as<Vec3>();
node["end"] = Vec3(2, -1, 0);
```
node["end"] = Vec3{2, -1, 0};
```

Observe that in the above example the custom type is, decalred as
a struct, explicit default constructable and all its members are
exposed. For non default constructable types like

```cpp
class NonDefCtorVec3 : public Vec3 {
using Vec3::x;
using Vec3::y;
using Vec3::z;
public:
NonDefCtorVec3(double x, double y, double z)
: Vec3() { this->x=x; this->y=y; this->z=z;
};
};
```
a new API is available, that freshens up the signature of the 'convert<T>::decode'
method and introduces the abortion of the deserialization process by throwing
an `DecodeException`.

```cpp
namespace YAML {
template <>
struct convert<NonDefCtorVec3> {
static Node encode(const NonDefCtorVec3& rhs) {
return convert<Vec3>::encode(rhs);
}

static NonDefCtorVec3 decode(const Node& node) {
if (!node.IsSequence() || node.size() != 3) {
throw YAML::conversion::DecodeException();
}
return {node[0].as<double>(), node[1].as<double>(), node[2].as<double>()};
}
};
}
```

The behavior is exactly the same

```cpp
YAML::Node node = YAML::Load("start: [1, 3, 0]");
NonDefCtorVec3 v = node["start"].as<NonDefCtorVec3>();
node["end"] = NonDefCtorVec3(2, -1, 0);
```
82 changes: 68 additions & 14 deletions include/yaml-cpp/node/detail/impl.h
Original file line number Diff line number Diff line change
@@ -96,30 +96,84 @@ struct remove_idx<Key,
}
};


//shim to emulate constexpr-if, which is a feature of c++17
#if __cplusplus < 201703L || (defined(_MSVC_LANG) && _MSVC_LANG < 201703L)
#define PRE_CPP17_SHIM
#endif

#ifdef PRE_CPP17_SHIM
template <bool AorB>
struct static_switch;

template<> //new api
struct static_switch<true> {
template<class T>
static T call(const Node& node) {
return convert<T>::decode(node);
}
};

template<> //old api
struct static_switch<false> {
template<class T>
static T call(const Node& node) {
T t;
if (convert<T>::decode(node, t))
return t;
throw conversion::DecodeException();
}
};
#endif

//detect the method of the new api
template <typename>
std::false_type has_decode_new_api(long);

template <typename T>
auto has_decode_new_api(int)
-> decltype( T::decode(std::declval<const Node&>()), std::true_type{});



template <typename T>
inline bool node::equals(const T& rhs, shared_memory_holder pMemory) {

try {
const auto rslt = convert<T>::decode(Node(*this, pMemory));
return rslt == rhs;
} catch (const conversion::DecodeException& e) {
return false;
} catch(...) {
std::rethrow_exception(std::current_exception());
#ifdef PRE_CPP17_SHIM
return static_switch<decltype(has_decode_new_api<convert<T>>(
0))::value>::template call<T>(Node(*this, pMemory)) == rhs;
#else
if constexpr (decltype(has_decode_new_api<convert<T>>(0))::value >)
return convert<T>::decode(Node(*this, pMemory)) == rhs;
else {
T lhs;
if (convert<T>::decode(Node(*this, pMemory), lhs))
return lhs == rhs;
throw conversion::DecodeException();
}
#endif
} catch(const conversion::DecodeException& e) {
//throw; //prefer to throw over returning just the inability to deserialize
return false; //not doing this breaks upstream functionality
} catch (...) {
throw;
}
}
#undef PRE_CPP17_SHIM


inline bool node::equals(const char* rhs, shared_memory_holder pMemory) {
try {
const auto rslt =
convert<std::string>::decode(Node(*this, std::move(pMemory)));
return rslt == rhs;
} catch (const conversion::DecodeException& e) {
return false;
} catch(...) {
std::rethrow_exception(std::current_exception());
std::string lhs;
if (convert<std::string>::decode(Node(*this, std::move(pMemory)), lhs)) {
return lhs == rhs;
}
return false;
}




// indexing
template <typename Key>
inline node* node_data::get(const Key& key,
79 changes: 71 additions & 8 deletions include/yaml-cpp/node/impl.h
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
#include "yaml-cpp/node/node.h"
#include <sstream>
#include <string>

#include <type_traits>

namespace YAML {
inline Node::Node()
@@ -88,6 +88,44 @@ inline NodeType::value Node::Type() const {

// access

//detect the method of the new api
template <typename>
std::false_type has_decode_new_api(long);

template <typename T>
auto has_decode_new_api(int)
-> decltype( T::decode(std::declval<const Node&>()), std::true_type{});

//shim to emulate constexpr-if, which is a feature of c++17
#if __cplusplus < 201703L || (defined(_MSVC_LANG) && _MSVC_LANG < 201703L)
#define PRE_CPP17_SHIM
#endif

#ifdef PRE_CPP17_SHIM
template <bool AorB>
struct static_switch;

template<> //new api
struct static_switch<true> {
template<class T>
static T call(const Node& node) {
return convert<T>::decode(node);
}
};

template<> //old api
struct static_switch<false> {
template<class T>
static T call(const Node& node) {
T t;
if (convert<T>::decode(node, t))
return t;
throw conversion::DecodeException();
}
};
#endif


// template helpers
template <typename T, typename S>
struct as_if {
@@ -99,13 +137,25 @@ struct as_if {
return fallback;

try {
return convert<T>::decode(node);
#ifdef PRE_CPP17_SHIM
return static_switch<decltype(has_decode_new_api<convert<T>>(
0))::value>::template call<T>(node);
#else
if constexpr (decltype(has_decodex<convert<T>>(0))::value >)
return convert<T>::decodex(node);
else {
T t;
if (convert<T>::decode(node, t))
return t;
throw conversion::DecodeException();
}
#endif
} catch (const conversion::DecodeException& e) {
return fallback;
} catch (...) {
std::rethrow_exception(std::current_exception());
throw;
}
}
};
};

//specialize for Node
@@ -125,7 +175,7 @@ struct as_if<Node, S> {
} catch (const conversion::DecodeException& e) {
return fallback;
} catch (...) {
std::rethrow_exception(std::current_exception());
throw;
}
}
};
@@ -154,14 +204,27 @@ struct as_if<T, void> {
throw TypedBadConversion<T>(node.Mark());

try {
return convert<T>::decode(node);
#ifdef PRE_CPP17_SHIM
return static_switch<decltype(has_decode_new_api<convert<T>>(
0))::value>::template call<T>(node);
#else
if constexpr (decltype(has_decodex<convert<T>>(0))::value >)
return convert<T>::decodex(node);
else {
T t;
if (convert<T>::decode(node, t))
return t;
throw conversion::DecodeException();
}
#endif
} catch(const conversion::DecodeException& e) {
throw TypedBadConversion<T>(node.Mark());
throw TypedBadConversion<T>(node.Mark());
} catch (...) {
std::rethrow_exception(std::current_exception());
throw;
}
};
};
#undef PRE_CPP17_SHIM

template <>
struct as_if<std::string, void> {
1 change: 1 addition & 0 deletions include/yaml-cpp/node/node.h
Original file line number Diff line number Diff line change
@@ -143,6 +143,7 @@ YAML_CPP_API bool operator==(const Node& lhs, const Node& rhs);

YAML_CPP_API Node Clone(const Node& node);

//forward declare this customization point for all types
template <typename T>
struct convert;
}
81 changes: 81 additions & 0 deletions test/node/node_test.cpp
Original file line number Diff line number Diff line change
@@ -42,6 +42,30 @@ template <class T> using CustomList = std::list<T,CustomAllocator<T>>;
template <class K, class V, class C=std::less<K>> using CustomMap = std::map<K,V,C,CustomAllocator<std::pair<const 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>>>;

struct Vec3 {
double x, y, z;
bool operator==(const Vec3& rhs) const {
return x == rhs.x && y == rhs.y && z == rhs.z;
}
};

class NonDefCtorVec3 : public Vec3 {
using Vec3::x;
using Vec3::y;
using Vec3::z;
public:
NonDefCtorVec3(double x, double y, double z)
: Vec3() {
this->x=x;
this->y=y;
this->z=z;
};
bool operator==(const NonDefCtorVec3& rhs) const {
return x == rhs.x && y == rhs.y && z == rhs.z;
}
};


} // anonymous namespace

using ::testing::AnyOf;
@@ -56,6 +80,45 @@ using ::testing::Eq;
}

namespace YAML {

//define custom convert structs
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;
}
};

template <>
struct convert<NonDefCtorVec3> {
static Node encode(const NonDefCtorVec3& rhs) {
return convert<Vec3>::encode(rhs);
}

static NonDefCtorVec3 decode(const Node& node) {
if (!node.IsSequence() || node.size() != 3) {
throw YAML::conversion::DecodeException();
}
return {node[0].as<double>(), node[1].as<double>(), node[2].as<double>()};
}
};


namespace {
TEST(NodeTest, SimpleScalar) {
Node node = Node("Hello, World!");
@@ -674,6 +737,24 @@ TEST(NodeTest, AccessNonexistentKeyOnConstNode) {
ASSERT_FALSE(other["5"]);
}

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) {
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}));
}

class NodeEmitterTest : public ::testing::Test {
protected:
void ExpectOutput(const std::string& output, const Node& node) {