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
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile
- [JSON schema](#json-schema)
- [Enums](#enums)
- [Algebraic data types](#algebraic-data-types)
- [Extra fields](#extra-fields)
- [Extra fields](#extra-fields)
- [std::expected](#stdexpected)
- [Reflective programming](#reflective-programming)
- [Standard Library Integration](#support-for-containers)
- [The team behind reflect-cpp](#the-team-behind-reflect-cpp)
Expand Down Expand Up @@ -509,6 +510,52 @@ This results in the following JSON string:
{"firstName":"Homer","lastName":"Simpson","age":45,"email":"homer@simpson.com","town":"Springfield"}
```

### std::expected

reflect-cpp also supports C++-23's `std::expected`:

```cpp
#include <expected>
#include <rfl/json.hpp>

// A success value is serialized like the value itself:
const std::expected<int, std::string> age = 45;
const std::string json_string = rfl::json::write(age);
// -> 45

// An error is serialized as an object with a single "error" field:
const std::expected<int, std::string> no_age = std::unexpected("unknown age");
const std::string json_string2 = rfl::json::write(no_age);
// -> {"error":"unknown age"}

const auto age2 =
rfl::json::read<std::expected<int, std::string>>(json_string).value();
const auto no_age2 =
rfl::json::read<std::expected<int, std::string>>(json_string2).value();
```

`std::expected` can be used anywhere other types can be used, for example as a
field of a struct or inside a container:

```cpp
struct Person {
std::string first_name;
std::vector<std::expected<int, std::string>> ages;
};

const auto homer =
Person{.first_name = "Homer",
.ages = {42, std::unexpected("unknown age")}};

const std::string json_string3 = rfl::json::write(homer);
// -> {"first_name":"Homer","ages":[42,{"error":"unknown age"}]}
```

`std::expected` requires a standard library that provides the C++-23 feature
(feature-test macro `__cpp_lib_expected`). Note that `std::expected<void, E>` and
`std::expected<T, T>` with an identical value and error type are not supported.
Refer to the [documentation](https://rfl.getml.com/expected) for details.

### Reflective programming

Beyond serialization and deserialization, reflect-cpp also supports reflective programming in general.
Expand Down Expand Up @@ -617,6 +664,7 @@ reflect-cpp supports the following containers from the C++ standard library:
- `std::atomic`
- `std::atomic_flag`
- `std::deque`
- `std::expected`
- `std::chrono::duration`
- `std::filesystem::path`
- `std::forward_list`
Expand Down Expand Up @@ -677,21 +725,6 @@ The following compilers are supported for C++-20:
The following compilers are supported for C++-26:
- GCC 16.2 or higher

### Compiling with C++-26 reflection

To compile reflect-cpp using the standard C++ reflection facilities, pass the CMake option
`REFLECTCPP_USE_CPP26_REFLECTION` together with the compiler flag that activates reflection
support in your compiler (`-freflection` for GCC, `-freflection-latest` for Clang):

```bash
cmake -S . -B build -DCMAKE_CXX_STANDARD=26 -DCMAKE_BUILD_TYPE=Release -DREFLECTCPP_USE_CPP26_REFLECTION=ON -DCMAKE_CXX_FLAGS="-freflection"
cmake --build build -j 4
```

With C++-26 reflection, fixed-size C arrays and inheritance are supported out of the box (no
`-DREFLECT_CPP_C_ARRAYS_OR_INHERITANCE` flag needed), and there are no range restrictions for
enums. Refer to the [documentation](https://rfl.getml.com/cpp26_reflection) for details.

### Using vcpkg

https://vcpkg.io/en/package/reflectcpp
Expand Down Expand Up @@ -729,6 +762,21 @@ cmake --build build --config Release -j 4 # MSVC

For other installation methods, refer to the [documentation](https://rfl.getml.com/docs-readme).

### Compiling with C++-26 reflection

To compile reflect-cpp using the standard C++ reflection facilities, pass the CMake option
`REFLECTCPP_USE_CPP26_REFLECTION` together with the compiler flag that activates reflection
support in your compiler (`-freflection` for GCC, `-freflection-latest` for Clang):

```bash
cmake -S . -B build -DCMAKE_CXX_STANDARD=26 -DCMAKE_BUILD_TYPE=Release -DREFLECTCPP_USE_CPP26_REFLECTION=ON -DCMAKE_CXX_FLAGS="-freflection"
cmake --build build -j 4
```

With C++-26 reflection, fixed-size C arrays and inheritance are supported out of the box (no
`-DREFLECT_CPP_C_ARRAYS_OR_INHERITANCE` flag needed), and there are no range restrictions for
enums. Refer to the [documentation](https://rfl.getml.com/cpp26_reflection) for details.

## The team behind reflect-cpp

reflect-cpp has been developed by [getML (Code17 GmbH)](https://getml.com), a company specializing in software engineering and machine learning for enterprise applications. reflect-cpp is currently maintained by Patrick Urbanke and Manuel Bellersen, with major contributions coming from the community.
Expand Down
2 changes: 2 additions & 0 deletions docs/docs-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

[Standard containers](standard_containers.md) - Describes how reflect-cpp treats containers in the standard library.

[std::expected](expected.md) - For serializing and deserializing `std::expected`, the C++-23 result type.

[C arrays and inheritance](c_arrays_and_inheritance.md) - Describes how reflect-cpp handles C arrays and inheritance.

[rfl::Bytestring](bytestring.md) - Describes how reflect-cpp handles binary strings for formats that support them.
Expand Down
108 changes: 108 additions & 0 deletions docs/expected.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# `std::expected`

C++-23 introduced `std::expected` as the standard way of expressing the result of an operation that might fail. Unlike `std::optional`, it holds both the value on success (of type `T`) and the error on failure (of type `E`). reflect-cpp supports `std::expected` out of the box: you can use it as a top-level type, as a field of a struct, or inside containers.

## Availability

`std::expected` is a C++-23 feature. reflect-cpp detects it via the feature-test macro `__cpp_lib_expected`. Note that some standard libraries only expose `<expected>` when compiled in C++-23 mode — for instance, GCC's libstdc++ requires `-std=c++23`. If your standard library does not provide `std::expected`, reflect-cpp will not recognize the type, and attempting to serialize one will result in a compile-time error.

## How `std::expected` is serialized

A `std::expected<T, E>` is serialized as the two alternatives of an untagged variant (an `rfl::Variant`):

- If it holds a value, the value is written as-is, i.e. exactly the same way as it would be written if it were of type `T`.
- If it holds an error, an object with a single field named `error` is written, containing the value of type `E`.

Wrapping the error in an object makes sure that the error is always recognized as an object, even if `T` itself is a struct.

```cpp
#include <expected>
#include <rfl/json.hpp>

const std::expected<int, std::string> ok = 42;
const std::string ok_json = rfl::json::write(ok);
// -> 42

const std::expected<int, std::string> err = std::unexpected("Something went wrong.");
const std::string err_json = rfl::json::write(err);
// -> {"error":"Something went wrong."}
```

Reading works in the reverse direction:

```cpp
const auto ok2 = rfl::json::read<std::expected<int, std::string>>(ok_json).value();
const auto err2 = rfl::json::read<std::expected<int, std::string>>(err_json).value();

// ok2.value() == 42
// err2.error() == "Something went wrong."
```

## Inside structs and containers

`std::expected` can be used as a field type and inside containers, just like any other supported type:

```cpp
struct Person {
std::string name;
std::expected<int, std::string> age;
};

const Person homer = {.name = "Homer", .age = 42};
const Person maggie = {.name = "Maggie", .age = std::unexpected("too young")};

const std::string homer_json = rfl::json::write(homer);
// -> {"name":"Homer","age":42}

const std::string maggie_json = rfl::json::write(maggie);
// -> {"name":"Maggie","age":{"error":"too young"}}
```

Vectors of `std::expected` work as well:

```cpp
struct Person {
std::string first_name;
std::vector<std::expected<int, std::string>> ages;
};

const auto homer =
Person{.first_name = "Homer",
.ages = {42, std::unexpected("unknown age")}};

const std::string json = rfl::json::write(homer);
// -> {"first_name":"Homer","ages":[42,{"error":"unknown age"}]}
```

## Structs as value types

If `T` is a struct, the success case is serialized as the struct itself:

```cpp
const std::expected<Person, std::string> value =
Person{.first_name = "Bart", .ages = {10}};

const std::string json_string = rfl::json::write(value);
// -> {"first_name":"Bart","ages":[10]}
```

The error case is still serialized as an object with an `error` field, so the two alternatives remain unambiguous.

## JSON schema

Schemata are generated for `std::expected` as well. The schema of `std::expected<int, std::string>` looks like this:

```json
{"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"type":"integer"},{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}],"$defs":{}}
```

## Limitations

- `std::expected<void, E>` is not supported.
- Formats that do not support variants (CSV and Parquet) do not support `std::expected` either, since it is serialized as a variant under the hood.

## Relation to `rfl::Result`

reflect-cpp's own result type, [`rfl::Result`](result.md), is what `rfl::json::read` and `rfl::json::write` return and operate on. Supporting `std::expected` is a separate concern: it means that you can use `std::expected<T, E>` as a *data type* in your structs, which is what this section is about.

Note that there is a CMake option `REFLECTCPP_USE_STD_EXPECTED` that makes `rfl::Result<T>` an alias for `std::expected<T, rfl::Error>`. This is a separate feature from the one described in this section, but the two can be combined.
3 changes: 3 additions & 0 deletions docs/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,6 @@ const auto embellish_error = [&](const Error& _e) -> rfl::Result<T> {
return Parser<T>::read(_r, &_var).transform_error(embellish_error);
```

## See also

- [`std::expected`](expected.md) - The C++-23 standard result type, which is supported as a serializable data type.
4 changes: 3 additions & 1 deletion docs/standard_containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ This will also be represented as follows:
```

All other supported standard containers
(other than `std::variant`, `std::optional`, `std::unique_ptr` and `std::shared_ptr`)
(other than `std::variant`, `std::optional`, `std::unique_ptr`, `std::shared_ptr` and `std::expected`)
will be represented as arrays. Containers for which the `value_type`
is a key-value-pair will be represented as arrays of pairs.

`std::expected` is an exception to this: it is serialized as its value type, or as an object with a single `error` field. Refer to the [std::expected](expected.md) section for details.
9 changes: 5 additions & 4 deletions include/rfl/bson/write.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ Result<std::pair<uint8_t*, size_t>> to_buffer(const auto& _obj) noexcept {
const auto len = bson_writer_get_length(bson_writer.get());
return nothing
.transform([&](const auto&) { return std::make_pair(buf, len); })
.or_else([&](auto&& _err) {
bson_free(buf);
return error(_err.what());
});
.or_else(
[&](auto&& _err) -> Result<std::pair<uint8_t*, size_t>> {
bson_free(buf);
return error(_err.what());
});
}

/// Returns BSON bytes representation of the object.
Expand Down
113 changes: 113 additions & 0 deletions include/rfl/parsing/ParserExpected.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#ifndef RFL_PARSING_PARSER_EXPECTED_HPP_
#define RFL_PARSING_PARSER_EXPECTED_HPP_

#include <map>
#include <type_traits>

#include "../Field.hpp"
#include "../NamedTuple.hpp"
#include "../Result.hpp"
#include "../Variant.hpp"
#include "Parser_base.hpp"
#include "schema/Type.hpp"

#if __has_include(<expected>)
#include <expected>
#endif

namespace rfl::parsing {

template <class T>
struct is_expected : std::false_type {};

/// @brief Primary declaration; defined below if std::expected is available.
template <class R, class W, class ExpectedType, class ProcessorsType>
struct ParserExpected;

#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L

template <class T, class E>
struct is_expected<std::expected<T, E>> : std::true_type {};

/**
* @brief Parser specialization for std::expected.
*
* A std::expected<T, E> is serialized as an rfl::Variant<T, E>: the success
* value and the error are treated as the two alternatives of a variant.
*/
template <class R, class W, class ExpectedType, class ProcessorsType>
struct ParserExpected {
using T = std::remove_cvref_t<ExpectedType>;

using ValueType = typename T::value_type;

using ErrorType = typename T::error_type;

using WrappedErrorType = NamedTuple<Field<"error", ErrorType>>;

using VariantType = rfl::Variant<ValueType, WrappedErrorType>;

using InputVarType = typename R::InputVarType;

static_assert(!std::is_void_v<ValueType>,
"std::expected<void, E> is not supported by reflect-cpp.");

/**
* @brief Reads a std::expected from the input.
*
* @param _r The reader to use.
* @param _var The input variable to read from.
* @return A Result containing the parsed std::expected or an error.
*/
static Result<T> read(const R& _r, const InputVarType& _var) noexcept {
const auto to_expected = [](auto&& _alternative) -> T {
using AltType = std::remove_cvref_t<decltype(_alternative)>;
if constexpr (std::is_same_v<AltType, WrappedErrorType>) {
return T(std::unexpected(
std::forward<ErrorType>(_alternative.template get<"error">())));
} else {
return T(std::forward<ValueType>(_alternative));
}
};
return Parser<R, W, VariantType, ProcessorsType>::read(_r, _var).transform(
[&](auto&& _variant) -> T {
return std::forward<VariantType>(_variant).visit(to_expected);
});
}

/**
* @brief Writes a std::expected to the output.
*
* @tparam P The type of the parent.
* @param _w The writer to use.
* @param _var The std::expected to write.
* @param _parent The parent object.
*/
template <class P>
static void write(const W& _w, const T& _var, const P& _parent) {
const VariantType variant =
_var.has_value() ? VariantType(_var.value())
: VariantType(WrappedErrorType(_var.error()));
Parser<R, W, VariantType, ProcessorsType>::write(_w, variant, _parent);
}

/**
* @brief Generates the schema for the std::expected.
*
* @param _definitions The map of definitions to add to.
* @return The schema type.
*/
static schema::Type to_schema(
std::map<std::string, schema::Type>* _definitions) {
return Parser<R, W, VariantType, ProcessorsType>::to_schema(_definitions);
}
};

#endif // __cpp_lib_expected

template <class T>
constexpr bool is_expected_v = is_expected<std::remove_cvref_t<T>>::value;

} // namespace rfl::parsing

#endif
Loading
Loading