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

implement skip writing out default #1438

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion include/glaze/core/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ namespace glz::detail
template <opts Opts, class Value>
[[nodiscard]] GLZ_ALWAYS_INLINE constexpr bool skip_member(const Value& value) noexcept
{
if constexpr (null_t<Value> && Opts.skip_null_members) {
if constexpr (null_t<Value> && (Opts.skip_null_members & skip_null_flag)) {
if constexpr (always_null_t<Value>)
return true;
else {
Expand Down
5 changes: 4 additions & 1 deletion include/glaze/core/opts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ namespace glz
#define GLZ_NULL_TERMINATED true
#endif

inline constexpr uint8_t skip_null_flag = 1;
inline constexpr uint8_t skip_default_flag = 2;
struct opts
{
// USER CONFIGURABLE
uint32_t format = JSON;
bool_t null_terminated = GLZ_NULL_TERMINATED; // Whether the input buffer is null terminated
bool_t comments = false; // Support reading in JSONC style comments
bool_t error_on_unknown_keys = true; // Error when an unknown key is encountered
bool_t skip_null_members = true; // Skip writing out params in an object if the value is null
// this might be better to be named skip_write_members, just keep for backward compatibility
uint8_t skip_null_members = skip_null_flag | skip_default_flag; // Skip writing out params in an object if the value is null or default
bool_t use_hash_comparison = true; // Will replace some string equality checks with hash checks
bool_t prettify = false; // Write out prettified JSON
bool_t minified = false; // Require minified input for JSON, which results in faster read performance
Expand Down
15 changes: 13 additions & 2 deletions include/glaze/core/reflect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ namespace glz
static constexpr bool maybe_skipped = false;
};

template <typename T>
constexpr uint8_t skip_write_mask() {
if constexpr (detail::null_t<T>) {
return skip_null_flag;
} else if constexpr (requires { T::glaze_skip_write_mask; }) {
return T::glaze_skip_write_mask;
} else {
return 0;
}
}

template <opts Opts, class T>
requires(reflect<T>::size > 0)
struct object_info<Opts, T>
Expand All @@ -288,7 +299,7 @@ namespace glz
if constexpr (N > 0) {
using V = std::remove_cvref_t<refl_t<T, 0>>;

if constexpr (detail::null_t<V> && Opts.skip_null_members) {
if constexpr (skip_write_mask<V>() & Opts.skip_null_members ) {
return false;
}

Expand All @@ -310,7 +321,7 @@ namespace glz
for_each_short_circuit<N>([&](auto I) {
using V = std::remove_cvref_t<refl_t<T, I>>;

if constexpr (Opts.skip_null_members && detail::null_t<V>) {
if constexpr (skip_write_mask<V>() & Opts.skip_null_members) {
found_maybe_skipped = true;
return true; // early exit
}
Expand Down
48 changes: 40 additions & 8 deletions include/glaze/core/wrappers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,52 @@ namespace glz
};
}

struct always_write_t
{};

// custom_t allows a user to register member functions (and std::function members) to implement custom reading and
// writing
template <class T, class From, class To>
template <class T, class From, class To, class Skippable, class SkipMask>
struct custom_t final
{
static constexpr auto glaze_reflect = false;
static constexpr auto glaze_wrapper = true;
static constexpr auto glaze_skip_write_mask = SkipMask::value;
using from_t = From;
using to_t = To;
T& val;
From from;
To to;
constexpr bool write_skippable() const { return Skippable()(val); }
};

template <class T, class From, class To>
custom_t(T&, From, To) -> custom_t<T, From, To>;

template <auto From, auto To>
template <auto From, auto To, auto WriteSkippable, uint8_t SkipMask>
inline constexpr auto custom_impl() noexcept
{
return [](auto&& v) { return custom_t{v, From, To}; };
return [](auto&& v) {
using skippable_t = std::decay_t<decltype(WriteSkippable)>;
using skip_mask_t =
std::conditional_t<std::is_same_v<skippable_t, always_write_t>, std::integral_constant<uint8_t, 0>,
std::integral_constant<uint8_t, SkipMask>>;
return custom_t<std::remove_reference_t<decltype(v)>, std::decay_t<decltype(From)>,
std::decay_t<decltype(To)>, skippable_t, skip_mask_t>{v, From, To};
};
}

// Used for determining the default value
struct deduct_default_t
{};

template <auto MemPtr, auto Default, class T>
bool is_default(const T& val)
{
if constexpr (std::same_as<std::decay_t<decltype(Default)>, deduct_default_t>) {
static thread_local auto defaulted = T{};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe the thread_local is unnecessary because C++ requires static variable initialization to be thread safe since C++11. The statement might be better as follows.

static const auto defaulted = T{}.*MemPtr;
return val.*MemPtr == defaulted;

I expected the compiler can be smart enough not to construct unused data members this way; however, based on my experiment on compiler explorer, no compiler can do that.

Copy link
Owner

Choose a reason for hiding this comment

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

You are correct, because we aren't changing state, we can have a single, global static variable. I'm just used to requiring thread_local for mutation. We can also make it const.

return val.*MemPtr == defaulted.*MemPtr;
}
else {
return val.*MemPtr == Default;
}
}
}

Expand All @@ -82,6 +108,12 @@ namespace glz
template <auto MemPtr>
constexpr auto partial_read = detail::opts_wrapper<MemPtr, &opts::partial_read>();

template <auto From, auto To>
constexpr auto custom = detail::custom_impl<From, To>();
template <auto From, auto To, auto WriteSkippable = detail::always_write_t{}, uint8_t SkipMask = UINT8_MAX>
constexpr auto custom = detail::custom_impl<From, To, WriteSkippable, SkipMask>();

// Skip writing out default value
template <auto MemPtr, auto Default = detail::deduct_default_t{}>
constexpr auto skip_write_default =
detail::custom_impl<MemPtr, MemPtr, [](const auto& val) { return detail::is_default<MemPtr, Default>(val); },
skip_default_flag>();
}
43 changes: 26 additions & 17 deletions include/glaze/json/write.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1433,29 +1433,38 @@ namespace glz
return;
}
else {
if constexpr (null_t<val_t>) {
if constexpr (always_null_t<T>)
return;
else {
const auto is_null = [&]() {
decltype(auto) element = [&]() -> decltype(auto) {
if constexpr (reflectable<T>) {
return get<I>(t);
}
else {
return get<I>(reflect<T>::values);
}
};
if constexpr (null_t<val_t> && always_null_t<T>) {
return;
}
else if constexpr (skip_write_mask<val_t>() & Opts.skip_null_members) {
const auto to_skip = [&]() {
decltype(auto) element = [&]() -> decltype(auto) {
if constexpr (reflectable<T>) {
return get<I>(t);
}
else {
return get<I>(reflect<T>::values);
}
};

if constexpr (nullable_wrapper<val_t>) {
if constexpr (null_t<val_t>) {
if constexpr (glaze_wrapper<val_t>) {
return !bool(element()(value).val);
}
else {
return !bool(get_member(value, element()));
}
}();
if (is_null) return;
}
}
else {
if constexpr (glaze_wrapper<val_t>) {
return element()(value).write_skippable();
}
else {
return get_member(value, element()).write_skippable();
}
}
}();
if (to_skip) return;
}

maybe_pad<padding>(b, ix);
Expand Down
70 changes: 70 additions & 0 deletions tests/json_test/json_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9876,6 +9876,76 @@ suite const_pointer_tests = [] {
};
};

struct custom_skip_struct
{
int i = 287;
double j = 3.14;
std::optional<int> k;
};

template <>
struct glz::meta<custom_skip_struct>
{
using T = custom_skip_struct;
static constexpr auto skip_default_i = [](const T& t) { return t.i == 287; };
static constexpr auto value =
object("i", glz::custom<&T::i, &T::i, skip_default_i, skip_default_flag>, "j", &T::j, "k", &T::k);
};

struct skip_write_default_struct
{
int i = 287;
double j = 3.14;
std::optional<int> k;
};

template <>
struct glz::meta<skip_write_default_struct>
{
using T = skip_write_default_struct;
static constexpr auto value = object("i", glz::skip_write_default<&T::i>, "j", &T::j, "k", &T::k);
};

struct skip_write_default_heap_allocated_struct
{
std::string str = "Here is a decently long string to avoid short string optimization";
};

template <>
struct glz::meta<skip_write_default_heap_allocated_struct>
{
using T = skip_write_default_heap_allocated_struct;
static constexpr auto value = object("str", glz::skip_write_default<&T::str>);
};


suite skip_struct_test = [] {
auto skip_test = [](auto obj) {
std::string buffer{};
expect(not glz::write_json(obj, buffer));
// We expect both the default "i" and the null "k" to be skipped
expect(buffer == R"({"j":3.14})") << buffer;

expect(not glz::write<glz::opts{.skip_null_members = glz::skip_null_flag}>(obj, buffer));
// We expect only the null "k" to be skipped
expect(buffer == R"({"i":287,"j":3.14})") << buffer;

expect(not glz::write<glz::opts{.skip_null_members = glz::skip_default_flag}>(obj, buffer));
// We expect only the default "i" to be skipped
expect(buffer == R"({"j":3.14,"k":null})") << buffer;
};

"custom_skip_struct"_test = [&] { skip_test(custom_skip_struct{}); };
"skip_write_default_struct"_test = [&] { skip_test(skip_write_default_struct{}); };

"skip_write_default_heap_allocated_struct"_test = [] {
skip_write_default_heap_allocated_struct obj{};
std::string buffer{};
expect(not glz::write<glz::opts{.skip_null_members = glz::skip_default_flag}>(obj, buffer));
expect(buffer == R"({})") << buffer;
};
};

int main()
{
trace.end("json_test");
Expand Down