Skip to content
Merged
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
64 changes: 64 additions & 0 deletions docs/default_val.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Default values (rfl::DefaultVal)

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

## Declaration and initialization

You can declare a default-valued field like this:

```cpp
struct Person {
std::string first_name; // required
rfl::DefaultVal<std::string> last_name = "Simpson"; // has explicit default
rfl::DefaultVal<std::string> town; // default-constructed (empty string)
};
```

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

```cpp
Person p;
p.last_name = "Smith"; // assign underlying value
p.town = rfl::Default{}; // reset to default (empty string)
std::string s = p.last_name.value();
```

API convenience:

- .get(), .value(), operator()() — access the underlying value (const and non-const overloads).
- set(...) — assign underlying value.

## JSON behaviour

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

Example (object fields):

```cpp
// Person from above
const auto homer = rfl::json::read<Person>(R"({"first_name":"Homer"})").value();
// homer.last_name == "Simpson" (declared default)
// homer.town == "" (default-constructed)
```

Example (no field names / positional arrays):

DefaultVal also works when using rfl::NoFieldNames (positional JSON arrays). Omitted positions that correspond to DefaultVal fields get their default values:

```cpp
const auto homer = rfl::json::read<Person, rfl::NoFieldNames>(R"(["Homer"])" ).value();
// homer.first_name == "Homer"
// homer.last_name == "Simpson"
// homer.town == ""
```

## When to use

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

## Notes

- The underlying type must be default-constructible to allow resetting via `rfl::Default` or when no explicit default is supplied.
- 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).

For more advanced control over when fields are considered missing and how defaults are applied, see the processors documentation (e.g., `rfl::DefaultIfMissing`).
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ reflect-cpp fills an important gap in C++ development. It minimizes boilerplate
- Simple [installation](https://rfl.getml.com/install)
- Simple extendability to [other serialization formats](https://rfl.getml.com/supported_formats/supporting_your_own_format)
- Simple extendability to [custom classes](https://rfl.getml.com/concepts/custom_classes)
- Support for default-valued fields via `rfl::DefaultVal` (see [Default values](default_val.md))
- Being one of the fastest serialization libraries in existence, as demonstrated by our [benchmarks](https://rfl.getml.com/benchmarks)


Expand Down
1 change: 1 addition & 0 deletions include/rfl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "rfl/Box.hpp"
#include "rfl/Bytestring.hpp"
#include "rfl/DefaultIfMissing.hpp"
#include "rfl/DefaultVal.hpp"
#include "rfl/Description.hpp"
#include "rfl/ExtraFields.hpp"
#include "rfl/Field.hpp"
Expand Down
129 changes: 129 additions & 0 deletions include/rfl/DefaultVal.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#ifndef RFL_DEFAULTVAL_HPP_
#define RFL_DEFAULTVAL_HPP_

#include <type_traits>
#include <utility>

#include "default.hpp"

namespace rfl {

template <class T>
struct DefaultVal {
public:
using Type = std::remove_cvref_t<T>;

DefaultVal() : value_(Type()) {}

DefaultVal(const Type& _value) : value_(_value) {}

DefaultVal(Type&& _value) noexcept : value_(std::move(_value)) {}

DefaultVal(DefaultVal&& _field) noexcept = default;

DefaultVal(const DefaultVal& _field) = default;

template <class U>
DefaultVal(const DefaultVal<U>& _field) : value_(_field.get()) {}

Comment on lines +27 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This unconstrained converting copy constructor is redundant with the constrained version on lines 42-44. Having both can lead to ambiguity during template resolution. It would be safer to remove this one and rely solely on the constrained version.

template <class U>
DefaultVal(DefaultVal<U>&& _field) noexcept(
noexcept(Type(std::move(_field.value()))))
: value_(std::move(_field.value())) {}

template <class U>
requires(std::is_convertible_v<U, Type>)
DefaultVal(const U& _value) : value_(_value) {}

template <class U>
requires(std::is_convertible_v<U, Type>)
DefaultVal(U&& _value) noexcept : value_(std::forward<U>(_value)) {}

template <class U>
requires(std::is_convertible_v<U, Type>)
DefaultVal(const DefaultVal<U>& _field) : value_(_field.value()) {}

/// Assigns the underlying object to its default value.
template <class U = Type>
requires(std::is_default_constructible_v<U>)
DefaultVal(const Default&) : value_(Type()) {}

~DefaultVal() = default;

/// Returns the underlying object.
const Type& get() const { return value_; }

/// Returns the underlying object.
Type& operator()() { return value_; }

/// Returns the underlying object.
const Type& operator()() const { return value_; }

/// Assigns the underlying object.
auto& operator=(const Type& _value) {
value_ = _value;
return *this;
}

/// Assigns the underlying object.
auto& operator=(Type&& _value) noexcept {
value_ = std::move(_value);
return *this;
}

/// Assigns the underlying object.
template <class U, typename std::enable_if<std::is_convertible_v<U, Type>,
bool>::type = true>
auto& operator=(const U& _value) {
value_ = _value;
return *this;
}

/// Assigns the underlying object to its default value.
template <class U = Type,
typename std::enable_if<std::is_default_constructible_v<U>,
bool>::type = true>
auto& operator=(const Default&) {
value_ = Type();
return *this;
}

/// Assigns the underlying object.
DefaultVal& operator=(const DefaultVal& _field) = default;

/// Assigns the underlying object.
DefaultVal& operator=(DefaultVal&& _field) = default;

/// Assigns the underlying object.
template <class U>
auto& operator=(const DefaultVal<U>& _field) {
value_ = _field.get();
return *this;
}

/// Assigns the underlying object.
template <class U>
auto& operator=(DefaultVal<U>&& _field) {
value_ = std::forward<U>(_field.value_);
return *this;
Comment thread
liuzicheng1987 marked this conversation as resolved.
}

/// Assigns the underlying object.
void set(const Type& _value) { value_ = _value; }

/// Assigns the underlying object.
void set(Type&& _value) { value_ = std::move(_value); }

/// Returns the underlying object.
Type& value() { return value_; }

/// Returns the underlying object.
const Type& value() const { return value_; }

/// The underlying value.
Type value_;
};

} // namespace rfl

#endif
27 changes: 27 additions & 0 deletions include/rfl/internal/has_default_val_v.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifndef RFL_HASDEFAULTVALV_HPP_
#define RFL_HASDEFAULTVALV_HPP_
#include <type_traits>

#include "../NamedTuple.hpp"
#include "../named_tuple_t.hpp"
#include "is_default_val_v.hpp"

namespace rfl::internal {

template <class T>
struct HasDefaultVal;

template <class... Fields>
struct HasDefaultVal<NamedTuple<Fields...>> {
static constexpr bool value =
(false || ... ||
is_default_val_v<
std::remove_cvref_t<std::remove_pointer_t<typename Fields::Type>>>);
};

template <class T>
constexpr bool has_default_val_v = HasDefaultVal<named_tuple_t<T>>::value;

} // namespace rfl::internal

#endif
31 changes: 11 additions & 20 deletions include/rfl/internal/has_tag_v.hpp
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
#ifndef RFL_HASTAGV_HPP_
#define RFL_HASTAGV_HPP_

#include <cstdint>
#include <concepts>

namespace rfl {
namespace internal {
namespace rfl::internal {

template <class Wrapper>
class HasTag {
private:
template <class U>
static std::int64_t foo(...);

template <class U>
static std::int32_t foo(typename U::Tag*);

public:
static constexpr bool value =
sizeof(foo<Wrapper>(nullptr)) == sizeof(std::int32_t);
};
template <class T>
struct TagWrapper {};

/// Used for tagged unions - determines whether a struct as a Tag.
template <typename Wrapper>
constexpr bool has_tag_v = HasTag<Wrapper>::value;
template <typename T>
constexpr bool has_tag_v = requires() {
{
TagWrapper<typename T::Tag>{}
} -> std::same_as<TagWrapper<typename T::Tag>>;
};

} // namespace internal
} // namespace rfl
} // namespace rfl::internal

#endif
25 changes: 25 additions & 0 deletions include/rfl/internal/is_default_val_v.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#ifndef RFL_INTERNAL_ISDEFAULTVAL_HPP_
#define RFL_INTERNAL_ISDEFAULTVAL_HPP_

#include <type_traits>

#include "../DefaultVal.hpp"

namespace rfl::internal {

template <class T>
class is_default_val;

template <class T>
class is_default_val : public std::false_type {};

template <class T>
class is_default_val<DefaultVal<T>> : public std::true_type {};

template <class T>
constexpr bool is_default_val_v =
is_default_val<std::remove_cvref_t<std::remove_pointer_t<T>>>::value;

} // namespace rfl::internal

#endif
14 changes: 12 additions & 2 deletions include/rfl/parsing/NamedTupleParser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
#include "../NamedTuple.hpp"
#include "../Result.hpp"
#include "../always_false.hpp"
#include "../internal/has_default_val_v.hpp"
#include "../internal/is_array.hpp"
#include "../internal/is_attribute.hpp"
#include "../internal/is_basic_type.hpp"
#include "../internal/is_default_val_v.hpp"
#include "../internal/is_extra_fields.hpp"
#include "../internal/is_skip.hpp"
#include "../internal/no_duplicate_field_names.hpp"
Expand Down Expand Up @@ -108,7 +110,6 @@ struct NamedTupleParser {
auto arr = _r.to_array(_var);
if (!arr) [[unlikely]] {
auto set = std::array<bool, NamedTupleType::size()>{};
// return std::make_pair(set, arr.error());
return std::make_pair(set, arr.error());
}
return read_object_or_array(_r, *arr, _view);
Expand Down Expand Up @@ -254,16 +255,19 @@ struct NamedTupleParser {

if (!std::get<_i>(_found)) {
constexpr bool is_required_field =
!internal::is_default_val_v<ValueType> &&
!internal::is_extra_fields_v<ValueType> &&
(_all_required || is_required<ValueType, _ignore_empty_containers>());

if constexpr (is_required_field) {
constexpr auto current_name =
internal::nth_element_t<_i, FieldTypes...>::name();
std::stringstream stream;
stream << "Field named '" << std::string(current_name)
<< "' not found.";
_errors->emplace_back(Error(stream.str()));
} else {

} else if constexpr (!internal::has_default_val_v<NamedTupleType>) {
if constexpr (!std::is_const_v<ValueType>) {
::new (rfl::get<_i>(_view)) ValueType();
} else {
Expand Down Expand Up @@ -338,9 +342,15 @@ struct NamedTupleParser {
return err;
}
}
if constexpr (internal::has_default_val_v<NamedTupleType> &&
!ProcessorsType::default_if_missing_) {
handle_missing_fields(reader.found(), *_view, nullptr, &errors,
std::make_integer_sequence<int, size_>());
}
if (errors.size() != 0) {
return to_single_error_message(errors);
}

return std::nullopt;
}
};
Expand Down
1 change: 1 addition & 0 deletions include/rfl/parsing/Parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "Parser_bytestring.hpp"
#include "Parser_c_array.hpp"
#include "Parser_default.hpp"
#include "Parser_default_val.hpp"
#include "Parser_duration.hpp"
#include "Parser_filepath.hpp"
#include "Parser_map_like.hpp"
Expand Down
4 changes: 3 additions & 1 deletion include/rfl/parsing/Parser_default.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "../always_false.hpp"
#include "../enums.hpp"
#include "../from_named_tuple.hpp"
#include "../internal/has_default_val_v.hpp"
#include "../internal/has_reflection_method_v.hpp"
#include "../internal/has_reflection_type_v.hpp"
#include "../internal/has_reflector.hpp"
Expand Down Expand Up @@ -79,7 +80,8 @@ struct Parser {
.and_then(wrap_in_t);

} else if constexpr (std::is_class_v<T> && std::is_aggregate_v<T>) {
if constexpr (ProcessorsType::default_if_missing_) {
if constexpr (ProcessorsType::default_if_missing_ ||
internal::has_default_val_v<T>) {
return read_struct_with_default(_r, _var);
} else {
return read_struct(_r, _var);
Expand Down
Loading
Loading