Skip to content

Commit 192a74e

Browse files
Added rfl::DefaultVal, resolves #534 (#553)
1 parent 3971eff commit 192a74e

15 files changed

Lines changed: 400 additions & 23 deletions

docs/default_val.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Default values (rfl::DefaultVal)
2+
3+
The `rfl::DefaultVal<T>` wrapper allows a struct field to have a predefined default value when serializing and deserializing. When a field is declared as `rfl::DefaultVal<T>`, the library will accept input that omits that field and will populate it with the provided default (or with a default-constructed T when no explicit default is given).
4+
5+
## Declaration and initialization
6+
7+
You can declare a default-valued field like this:
8+
9+
```cpp
10+
struct Person {
11+
std::string first_name; // required
12+
rfl::DefaultVal<std::string> last_name = "Simpson"; // has explicit default
13+
rfl::DefaultVal<std::string> town; // default-constructed (empty string)
14+
};
15+
```
16+
17+
DefaultVal behaves like a thin wrapper around the underlying type. You can construct and assign it from the underlying type, from other DefaultVal instances (if convertible), or assign the special token `rfl::Default` to reset it to the default-constructed value (if the type is default-constructible):
18+
19+
```cpp
20+
Person p;
21+
p.last_name = "Smith"; // assign underlying value
22+
p.town = rfl::Default{}; // reset to default (empty string)
23+
std::string s = p.last_name.value();
24+
```
25+
26+
API convenience:
27+
28+
- .get(), .value(), operator()() — access the underlying value (const and non-const overloads).
29+
- set(...) — assign underlying value.
30+
31+
## JSON behaviour
32+
33+
When writing JSON, fields that are DefaultVal are written like normal fields using their current underlying value. When reading JSON, omitted DefaultVal fields are filled with the default value (the value assigned in the declaration, or the type's default-constructed value).
34+
35+
Example (object fields):
36+
37+
```cpp
38+
// Person from above
39+
const auto homer = rfl::json::read<Person>(R"({"first_name":"Homer"})").value();
40+
// homer.last_name == "Simpson" (declared default)
41+
// homer.town == "" (default-constructed)
42+
```
43+
44+
Example (no field names / positional arrays):
45+
46+
DefaultVal also works when using rfl::NoFieldNames (positional JSON arrays). Omitted positions that correspond to DefaultVal fields get their default values:
47+
48+
```cpp
49+
const auto homer = rfl::json::read<Person, rfl::NoFieldNames>(R"(["Homer"])" ).value();
50+
// homer.first_name == "Homer"
51+
// homer.last_name == "Simpson"
52+
// homer.town == ""
53+
```
54+
55+
## When to use
56+
57+
Use rfl::DefaultVal when you want a field to be optional at the input side but still available as a value on the resulting object (no std::optional or pointer indirection). It is particularly useful for fields with sensible defaults (for example, a common last name, a default configuration value, or empty containers/strings).
58+
59+
## Notes
60+
61+
- The underlying type must be default-constructible to allow resetting via `rfl::Default` or when no explicit default is supplied.
62+
- DefaultVal preserves normal read/write semantics; other fields that are not DefaultVal remain required unless expressed as optionals or handled by processors (e.g., rfl::DefaultIfMissing).
63+
64+
For more advanced control over when fields are considered missing and how defaults are applied, see the processors documentation (e.g., `rfl::DefaultIfMissing`).

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ reflect-cpp fills an important gap in C++ development. It minimizes boilerplate
3434
- Simple [installation](https://rfl.getml.com/install)
3535
- Simple extendability to [other serialization formats](https://rfl.getml.com/supported_formats/supporting_your_own_format)
3636
- Simple extendability to [custom classes](https://rfl.getml.com/concepts/custom_classes)
37+
- Support for default-valued fields via `rfl::DefaultVal` (see [Default values](default_val.md))
3738
- Being one of the fastest serialization libraries in existence, as demonstrated by our [benchmarks](https://rfl.getml.com/benchmarks)
3839

3940

include/rfl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "rfl/Box.hpp"
1818
#include "rfl/Bytestring.hpp"
1919
#include "rfl/DefaultIfMissing.hpp"
20+
#include "rfl/DefaultVal.hpp"
2021
#include "rfl/Description.hpp"
2122
#include "rfl/ExtraFields.hpp"
2223
#include "rfl/Field.hpp"

include/rfl/DefaultVal.hpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#ifndef RFL_DEFAULTVAL_HPP_
2+
#define RFL_DEFAULTVAL_HPP_
3+
4+
#include <type_traits>
5+
#include <utility>
6+
7+
#include "default.hpp"
8+
9+
namespace rfl {
10+
11+
template <class T>
12+
struct DefaultVal {
13+
public:
14+
using Type = std::remove_cvref_t<T>;
15+
16+
DefaultVal() : value_(Type()) {}
17+
18+
DefaultVal(const Type& _value) : value_(_value) {}
19+
20+
DefaultVal(Type&& _value) noexcept : value_(std::move(_value)) {}
21+
22+
DefaultVal(DefaultVal&& _field) noexcept = default;
23+
24+
DefaultVal(const DefaultVal& _field) = default;
25+
26+
template <class U>
27+
DefaultVal(const DefaultVal<U>& _field) : value_(_field.get()) {}
28+
29+
template <class U>
30+
DefaultVal(DefaultVal<U>&& _field) noexcept(
31+
noexcept(Type(std::move(_field.value()))))
32+
: value_(std::move(_field.value())) {}
33+
34+
template <class U>
35+
requires(std::is_convertible_v<U, Type>)
36+
DefaultVal(const U& _value) : value_(_value) {}
37+
38+
template <class U>
39+
requires(std::is_convertible_v<U, Type>)
40+
DefaultVal(U&& _value) noexcept : value_(std::forward<U>(_value)) {}
41+
42+
template <class U>
43+
requires(std::is_convertible_v<U, Type>)
44+
DefaultVal(const DefaultVal<U>& _field) : value_(_field.value()) {}
45+
46+
/// Assigns the underlying object to its default value.
47+
template <class U = Type>
48+
requires(std::is_default_constructible_v<U>)
49+
DefaultVal(const Default&) : value_(Type()) {}
50+
51+
~DefaultVal() = default;
52+
53+
/// Returns the underlying object.
54+
const Type& get() const { return value_; }
55+
56+
/// Returns the underlying object.
57+
Type& operator()() { return value_; }
58+
59+
/// Returns the underlying object.
60+
const Type& operator()() const { return value_; }
61+
62+
/// Assigns the underlying object.
63+
auto& operator=(const Type& _value) {
64+
value_ = _value;
65+
return *this;
66+
}
67+
68+
/// Assigns the underlying object.
69+
auto& operator=(Type&& _value) noexcept {
70+
value_ = std::move(_value);
71+
return *this;
72+
}
73+
74+
/// Assigns the underlying object.
75+
template <class U, typename std::enable_if<std::is_convertible_v<U, Type>,
76+
bool>::type = true>
77+
auto& operator=(const U& _value) {
78+
value_ = _value;
79+
return *this;
80+
}
81+
82+
/// Assigns the underlying object to its default value.
83+
template <class U = Type,
84+
typename std::enable_if<std::is_default_constructible_v<U>,
85+
bool>::type = true>
86+
auto& operator=(const Default&) {
87+
value_ = Type();
88+
return *this;
89+
}
90+
91+
/// Assigns the underlying object.
92+
DefaultVal& operator=(const DefaultVal& _field) = default;
93+
94+
/// Assigns the underlying object.
95+
DefaultVal& operator=(DefaultVal&& _field) = default;
96+
97+
/// Assigns the underlying object.
98+
template <class U>
99+
auto& operator=(const DefaultVal<U>& _field) {
100+
value_ = _field.get();
101+
return *this;
102+
}
103+
104+
/// Assigns the underlying object.
105+
template <class U>
106+
auto& operator=(DefaultVal<U>&& _field) {
107+
value_ = std::forward<U>(_field.value_);
108+
return *this;
109+
}
110+
111+
/// Assigns the underlying object.
112+
void set(const Type& _value) { value_ = _value; }
113+
114+
/// Assigns the underlying object.
115+
void set(Type&& _value) { value_ = std::move(_value); }
116+
117+
/// Returns the underlying object.
118+
Type& value() { return value_; }
119+
120+
/// Returns the underlying object.
121+
const Type& value() const { return value_; }
122+
123+
/// The underlying value.
124+
Type value_;
125+
};
126+
127+
} // namespace rfl
128+
129+
#endif
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#ifndef RFL_HASDEFAULTVALV_HPP_
2+
#define RFL_HASDEFAULTVALV_HPP_
3+
#include <type_traits>
4+
5+
#include "../NamedTuple.hpp"
6+
#include "../named_tuple_t.hpp"
7+
#include "is_default_val_v.hpp"
8+
9+
namespace rfl::internal {
10+
11+
template <class T>
12+
struct HasDefaultVal;
13+
14+
template <class... Fields>
15+
struct HasDefaultVal<NamedTuple<Fields...>> {
16+
static constexpr bool value =
17+
(false || ... ||
18+
is_default_val_v<
19+
std::remove_cvref_t<std::remove_pointer_t<typename Fields::Type>>>);
20+
};
21+
22+
template <class T>
23+
constexpr bool has_default_val_v = HasDefaultVal<named_tuple_t<T>>::value;
24+
25+
} // namespace rfl::internal
26+
27+
#endif

include/rfl/internal/has_tag_v.hpp

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
11
#ifndef RFL_HASTAGV_HPP_
22
#define RFL_HASTAGV_HPP_
33

4-
#include <cstdint>
4+
#include <concepts>
55

6-
namespace rfl {
7-
namespace internal {
6+
namespace rfl::internal {
87

9-
template <class Wrapper>
10-
class HasTag {
11-
private:
12-
template <class U>
13-
static std::int64_t foo(...);
14-
15-
template <class U>
16-
static std::int32_t foo(typename U::Tag*);
17-
18-
public:
19-
static constexpr bool value =
20-
sizeof(foo<Wrapper>(nullptr)) == sizeof(std::int32_t);
21-
};
8+
template <class T>
9+
struct TagWrapper {};
2210

2311
/// Used for tagged unions - determines whether a struct as a Tag.
24-
template <typename Wrapper>
25-
constexpr bool has_tag_v = HasTag<Wrapper>::value;
12+
template <typename T>
13+
constexpr bool has_tag_v = requires() {
14+
{
15+
TagWrapper<typename T::Tag>{}
16+
} -> std::same_as<TagWrapper<typename T::Tag>>;
17+
};
2618

27-
} // namespace internal
28-
} // namespace rfl
19+
} // namespace rfl::internal
2920

3021
#endif
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#ifndef RFL_INTERNAL_ISDEFAULTVAL_HPP_
2+
#define RFL_INTERNAL_ISDEFAULTVAL_HPP_
3+
4+
#include <type_traits>
5+
6+
#include "../DefaultVal.hpp"
7+
8+
namespace rfl::internal {
9+
10+
template <class T>
11+
class is_default_val;
12+
13+
template <class T>
14+
class is_default_val : public std::false_type {};
15+
16+
template <class T>
17+
class is_default_val<DefaultVal<T>> : public std::true_type {};
18+
19+
template <class T>
20+
constexpr bool is_default_val_v =
21+
is_default_val<std::remove_cvref_t<std::remove_pointer_t<T>>>::value;
22+
23+
} // namespace rfl::internal
24+
25+
#endif

include/rfl/parsing/NamedTupleParser.hpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
#include "../NamedTuple.hpp"
1111
#include "../Result.hpp"
1212
#include "../always_false.hpp"
13+
#include "../internal/has_default_val_v.hpp"
1314
#include "../internal/is_array.hpp"
1415
#include "../internal/is_attribute.hpp"
1516
#include "../internal/is_basic_type.hpp"
17+
#include "../internal/is_default_val_v.hpp"
1618
#include "../internal/is_extra_fields.hpp"
1719
#include "../internal/is_skip.hpp"
1820
#include "../internal/no_duplicate_field_names.hpp"
@@ -108,7 +110,6 @@ struct NamedTupleParser {
108110
auto arr = _r.to_array(_var);
109111
if (!arr) [[unlikely]] {
110112
auto set = std::array<bool, NamedTupleType::size()>{};
111-
// return std::make_pair(set, arr.error());
112113
return std::make_pair(set, arr.error());
113114
}
114115
return read_object_or_array(_r, *arr, _view);
@@ -254,16 +255,19 @@ struct NamedTupleParser {
254255

255256
if (!std::get<_i>(_found)) {
256257
constexpr bool is_required_field =
258+
!internal::is_default_val_v<ValueType> &&
257259
!internal::is_extra_fields_v<ValueType> &&
258260
(_all_required || is_required<ValueType, _ignore_empty_containers>());
261+
259262
if constexpr (is_required_field) {
260263
constexpr auto current_name =
261264
internal::nth_element_t<_i, FieldTypes...>::name();
262265
std::stringstream stream;
263266
stream << "Field named '" << std::string(current_name)
264267
<< "' not found.";
265268
_errors->emplace_back(Error(stream.str()));
266-
} else {
269+
270+
} else if constexpr (!internal::has_default_val_v<NamedTupleType>) {
267271
if constexpr (!std::is_const_v<ValueType>) {
268272
::new (rfl::get<_i>(_view)) ValueType();
269273
} else {
@@ -338,9 +342,15 @@ struct NamedTupleParser {
338342
return err;
339343
}
340344
}
345+
if constexpr (internal::has_default_val_v<NamedTupleType> &&
346+
!ProcessorsType::default_if_missing_) {
347+
handle_missing_fields(reader.found(), *_view, nullptr, &errors,
348+
std::make_integer_sequence<int, size_>());
349+
}
341350
if (errors.size() != 0) {
342351
return to_single_error_message(errors);
343352
}
353+
344354
return std::nullopt;
345355
}
346356
};

include/rfl/parsing/Parser.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "Parser_bytestring.hpp"
88
#include "Parser_c_array.hpp"
99
#include "Parser_default.hpp"
10+
#include "Parser_default_val.hpp"
1011
#include "Parser_duration.hpp"
1112
#include "Parser_filepath.hpp"
1213
#include "Parser_map_like.hpp"

include/rfl/parsing/Parser_default.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "../always_false.hpp"
99
#include "../enums.hpp"
1010
#include "../from_named_tuple.hpp"
11+
#include "../internal/has_default_val_v.hpp"
1112
#include "../internal/has_reflection_method_v.hpp"
1213
#include "../internal/has_reflection_type_v.hpp"
1314
#include "../internal/has_reflector.hpp"
@@ -79,7 +80,8 @@ struct Parser {
7980
.and_then(wrap_in_t);
8081

8182
} else if constexpr (std::is_class_v<T> && std::is_aggregate_v<T>) {
82-
if constexpr (ProcessorsType::default_if_missing_) {
83+
if constexpr (ProcessorsType::default_if_missing_ ||
84+
internal::has_default_val_v<T>) {
8385
return read_struct_with_default(_r, _var);
8486
} else {
8587
return read_struct(_r, _var);

0 commit comments

Comments
 (0)