Skip to content

Commit cfe9fa3

Browse files
Added support for std::expected
1 parent 4d99e54 commit cfe9fa3

11 files changed

Lines changed: 399 additions & 21 deletions

File tree

README.md

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile
4545
- [JSON schema](#json-schema)
4646
- [Enums](#enums)
4747
- [Algebraic data types](#algebraic-data-types)
48-
- [Extra fields](#extra-fields)
48+
- [Extra fields](#extra-fields)
49+
- [std::expected](#stdexpected)
4950
- [Reflective programming](#reflective-programming)
5051
- [Standard Library Integration](#support-for-containers)
5152
- [The team behind reflect-cpp](#the-team-behind-reflect-cpp)
@@ -509,6 +510,52 @@ This results in the following JSON string:
509510
{"firstName":"Homer","lastName":"Simpson","age":45,"email":"homer@simpson.com","town":"Springfield"}
510511
```
511512

513+
### std::expected
514+
515+
reflect-cpp also supports C++-23's `std::expected`:
516+
517+
```cpp
518+
#include <expected>
519+
#include <rfl/json.hpp>
520+
521+
// A success value is serialized like the value itself:
522+
const std::expected<int, std::string> age = 45;
523+
const std::string json_string = rfl::json::write(age);
524+
// -> 45
525+
526+
// An error is serialized as an object with a single "error" field:
527+
const std::expected<int, std::string> no_age = std::unexpected("unknown age");
528+
const std::string json_string2 = rfl::json::write(no_age);
529+
// -> {"error":"unknown age"}
530+
531+
const auto age2 =
532+
rfl::json::read<std::expected<int, std::string>>(json_string).value();
533+
const auto no_age2 =
534+
rfl::json::read<std::expected<int, std::string>>(json_string2).value();
535+
```
536+
537+
`std::expected` can be used anywhere other types can be used, for example as a
538+
field of a struct or inside a container:
539+
540+
```cpp
541+
struct Person {
542+
std::string first_name;
543+
std::vector<std::expected<int, std::string>> ages;
544+
};
545+
546+
const auto homer =
547+
Person{.first_name = "Homer",
548+
.ages = {42, std::unexpected("unknown age")}};
549+
550+
const std::string json_string3 = rfl::json::write(homer);
551+
// -> {"first_name":"Homer","ages":[42,{"error":"unknown age"}]}
552+
```
553+
554+
`std::expected` requires a standard library that provides the C++-23 feature
555+
(feature-test macro `__cpp_lib_expected`). Note that `std::expected<void, E>` and
556+
`std::expected<T, T>` with an identical value and error type are not supported.
557+
Refer to the [documentation](https://rfl.getml.com/expected) for details.
558+
512559
### Reflective programming
513560
514561
Beyond serialization and deserialization, reflect-cpp also supports reflective programming in general.
@@ -617,6 +664,7 @@ reflect-cpp supports the following containers from the C++ standard library:
617664
- `std::atomic`
618665
- `std::atomic_flag`
619666
- `std::deque`
667+
- `std::expected`
620668
- `std::chrono::duration`
621669
- `std::filesystem::path`
622670
- `std::forward_list`
@@ -677,21 +725,6 @@ The following compilers are supported for C++-20:
677725
The following compilers are supported for C++-26:
678726
- GCC 16.2 or higher
679727
680-
### Compiling with C++-26 reflection
681-
682-
To compile reflect-cpp using the standard C++ reflection facilities, pass the CMake option
683-
`REFLECTCPP_USE_CPP26_REFLECTION` together with the compiler flag that activates reflection
684-
support in your compiler (`-freflection` for GCC, `-freflection-latest` for Clang):
685-
686-
```bash
687-
cmake -S . -B build -DCMAKE_CXX_STANDARD=26 -DCMAKE_BUILD_TYPE=Release -DREFLECTCPP_USE_CPP26_REFLECTION=ON -DCMAKE_CXX_FLAGS="-freflection"
688-
cmake --build build -j 4
689-
```
690-
691-
With C++-26 reflection, fixed-size C arrays and inheritance are supported out of the box (no
692-
`-DREFLECT_CPP_C_ARRAYS_OR_INHERITANCE` flag needed), and there are no range restrictions for
693-
enums. Refer to the [documentation](https://rfl.getml.com/cpp26_reflection) for details.
694-
695728
### Using vcpkg
696729
697730
https://vcpkg.io/en/package/reflectcpp
@@ -729,6 +762,21 @@ cmake --build build --config Release -j 4 # MSVC
729762

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

765+
### Compiling with C++-26 reflection
766+
767+
To compile reflect-cpp using the standard C++ reflection facilities, pass the CMake option
768+
`REFLECTCPP_USE_CPP26_REFLECTION` together with the compiler flag that activates reflection
769+
support in your compiler (`-freflection` for GCC, `-freflection-latest` for Clang):
770+
771+
```bash
772+
cmake -S . -B build -DCMAKE_CXX_STANDARD=26 -DCMAKE_BUILD_TYPE=Release -DREFLECTCPP_USE_CPP26_REFLECTION=ON -DCMAKE_CXX_FLAGS="-freflection"
773+
cmake --build build -j 4
774+
```
775+
776+
With C++-26 reflection, fixed-size C arrays and inheritance are supported out of the box (no
777+
`-DREFLECT_CPP_C_ARRAYS_OR_INHERITANCE` flag needed), and there are no range restrictions for
778+
enums. Refer to the [documentation](https://rfl.getml.com/cpp26_reflection) for details.
779+
732780
## The team behind reflect-cpp
733781

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

docs/docs-readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

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

33+
[std::expected](expected.md) - For serializing and deserializing `std::expected`, the C++-23 result type.
34+
3335
[C arrays and inheritance](c_arrays_and_inheritance.md) - Describes how reflect-cpp handles C arrays and inheritance.
3436

3537
[rfl::Bytestring](bytestring.md) - Describes how reflect-cpp handles binary strings for formats that support them.

docs/expected.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# `std::expected`
2+
3+
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.
4+
5+
## Availability
6+
7+
`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.
8+
9+
## How `std::expected` is serialized
10+
11+
A `std::expected<T, E>` is serialized as the two alternatives of an untagged variant (an `rfl::Variant`):
12+
13+
- 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`.
14+
- If it holds an error, an object with a single field named `error` is written, containing the value of type `E`.
15+
16+
Wrapping the error in an object makes sure that the error is always recognized as an object, even if `T` itself is a struct.
17+
18+
```cpp
19+
#include <expected>
20+
#include <rfl/json.hpp>
21+
22+
const std::expected<int, std::string> ok = 42;
23+
const std::string ok_json = rfl::json::write(ok);
24+
// -> 42
25+
26+
const std::expected<int, std::string> err = std::unexpected("Something went wrong.");
27+
const std::string err_json = rfl::json::write(err);
28+
// -> {"error":"Something went wrong."}
29+
```
30+
31+
Reading works in the reverse direction:
32+
33+
```cpp
34+
const auto ok2 = rfl::json::read<std::expected<int, std::string>>(ok_json).value();
35+
const auto err2 = rfl::json::read<std::expected<int, std::string>>(err_json).value();
36+
37+
// ok2.value() == 42
38+
// err2.error() == "Something went wrong."
39+
```
40+
41+
## Inside structs and containers
42+
43+
`std::expected` can be used as a field type and inside containers, just like any other supported type:
44+
45+
```cpp
46+
struct Person {
47+
std::string name;
48+
std::expected<int, std::string> age;
49+
};
50+
51+
const Person homer = {.name = "Homer", .age = 42};
52+
const Person maggie = {.name = "Maggie", .age = std::unexpected("too young")};
53+
54+
const std::string homer_json = rfl::json::write(homer);
55+
// -> {"name":"Homer","age":42}
56+
57+
const std::string maggie_json = rfl::json::write(maggie);
58+
// -> {"name":"Maggie","age":{"error":"too young"}}
59+
```
60+
61+
Vectors of `std::expected` work as well:
62+
63+
```cpp
64+
struct Person {
65+
std::string first_name;
66+
std::vector<std::expected<int, std::string>> ages;
67+
};
68+
69+
const auto homer =
70+
Person{.first_name = "Homer",
71+
.ages = {42, std::unexpected("unknown age")}};
72+
73+
const std::string json = rfl::json::write(homer);
74+
// -> {"first_name":"Homer","ages":[42,{"error":"unknown age"}]}
75+
```
76+
77+
## Structs as value types
78+
79+
If `T` is a struct, the success case is serialized as the struct itself:
80+
81+
```cpp
82+
const std::expected<Person, std::string> value =
83+
Person{.first_name = "Bart", .ages = {10}};
84+
85+
const std::string json_string = rfl::json::write(value);
86+
// -> {"first_name":"Bart","ages":[10]}
87+
```
88+
89+
The error case is still serialized as an object with an `error` field, so the two alternatives remain unambiguous.
90+
91+
## JSON schema
92+
93+
Schemata are generated for `std::expected` as well. The schema of `std::expected<int, std::string>` looks like this:
94+
95+
```json
96+
{"$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"type":"integer"},{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}],"$defs":{}}
97+
```
98+
99+
## Limitations
100+
101+
- `std::expected<void, E>` is not supported.
102+
- 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.
103+
104+
## Relation to `rfl::Result`
105+
106+
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.
107+
108+
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.

docs/result.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,6 @@ const auto embellish_error = [&](const Error& _e) -> rfl::Result<T> {
152152
return Parser<T>::read(_r, &_var).transform_error(embellish_error);
153153
```
154154

155+
## See also
156+
157+
- [`std::expected`](expected.md) - The C++-23 standard result type, which is supported as a serializable data type.

docs/standard_containers.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ This will also be represented as follows:
5151
```
5252

5353
All other supported standard containers
54-
(other than `std::variant`, `std::optional`, `std::unique_ptr` and `std::shared_ptr`)
54+
(other than `std::variant`, `std::optional`, `std::unique_ptr`, `std::shared_ptr` and `std::expected`)
5555
will be represented as arrays. Containers for which the `value_type`
5656
is a key-value-pair will be represented as arrays of pairs.
57+
58+
`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.

include/rfl/bson/write.hpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ Result<std::pair<uint8_t*, size_t>> to_buffer(const auto& _obj) noexcept {
5353
const auto len = bson_writer_get_length(bson_writer.get());
5454
return nothing
5555
.transform([&](const auto&) { return std::make_pair(buf, len); })
56-
.or_else([&](auto&& _err) {
57-
bson_free(buf);
58-
return error(_err.what());
59-
});
56+
.or_else(
57+
[&](auto&& _err) -> Result<std::pair<uint8_t*, size_t>> {
58+
bson_free(buf);
59+
return error(_err.what());
60+
});
6061
}
6162

6263
/// Returns BSON bytes representation of the object.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#ifndef RFL_PARSING_PARSER_EXPECTED_HPP_
2+
#define RFL_PARSING_PARSER_EXPECTED_HPP_
3+
4+
#include <map>
5+
#include <type_traits>
6+
7+
#include "../Field.hpp"
8+
#include "../NamedTuple.hpp"
9+
#include "../Result.hpp"
10+
#include "../Variant.hpp"
11+
#include "Parser_base.hpp"
12+
#include "schema/Type.hpp"
13+
14+
#if __has_include(<expected>)
15+
#include <expected>
16+
#endif
17+
18+
namespace rfl::parsing {
19+
20+
template <class T>
21+
struct is_expected : std::false_type {};
22+
23+
/// @brief Primary declaration; defined below if std::expected is available.
24+
template <class R, class W, class ExpectedType, class ProcessorsType>
25+
struct ParserExpected;
26+
27+
#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L
28+
29+
template <class T, class E>
30+
struct is_expected<std::expected<T, E>> : std::true_type {};
31+
32+
/**
33+
* @brief Parser specialization for std::expected.
34+
*
35+
* A std::expected<T, E> is serialized as an rfl::Variant<T, E>: the success
36+
* value and the error are treated as the two alternatives of a variant.
37+
*/
38+
template <class R, class W, class ExpectedType, class ProcessorsType>
39+
struct ParserExpected {
40+
using T = std::remove_cvref_t<ExpectedType>;
41+
42+
using ValueType = typename T::value_type;
43+
44+
using ErrorType = typename T::error_type;
45+
46+
using WrappedErrorType = NamedTuple<Field<"error", ErrorType>>;
47+
48+
using VariantType = rfl::Variant<ValueType, WrappedErrorType>;
49+
50+
using InputVarType = typename R::InputVarType;
51+
52+
static_assert(!std::is_void_v<ValueType>,
53+
"std::expected<void, E> is not supported by reflect-cpp.");
54+
55+
/**
56+
* @brief Reads a std::expected from the input.
57+
*
58+
* @param _r The reader to use.
59+
* @param _var The input variable to read from.
60+
* @return A Result containing the parsed std::expected or an error.
61+
*/
62+
static Result<T> read(const R& _r, const InputVarType& _var) noexcept {
63+
const auto to_expected = [](auto&& _alternative) -> T {
64+
using AltType = std::remove_cvref_t<decltype(_alternative)>;
65+
if constexpr (std::is_same_v<AltType, WrappedErrorType>) {
66+
return T(std::unexpected(
67+
std::forward<ErrorType>(_alternative.template get<"error">())));
68+
} else {
69+
return T(std::forward<ValueType>(_alternative));
70+
}
71+
};
72+
return Parser<R, W, VariantType, ProcessorsType>::read(_r, _var).transform(
73+
[&](auto&& _variant) -> T {
74+
return std::forward<VariantType>(_variant).visit(to_expected);
75+
});
76+
}
77+
78+
/**
79+
* @brief Writes a std::expected to the output.
80+
*
81+
* @tparam P The type of the parent.
82+
* @param _w The writer to use.
83+
* @param _var The std::expected to write.
84+
* @param _parent The parent object.
85+
*/
86+
template <class P>
87+
static void write(const W& _w, const T& _var, const P& _parent) {
88+
const VariantType variant =
89+
_var.has_value() ? VariantType(_var.value())
90+
: VariantType(WrappedErrorType(_var.error()));
91+
Parser<R, W, VariantType, ProcessorsType>::write(_w, variant, _parent);
92+
}
93+
94+
/**
95+
* @brief Generates the schema for the std::expected.
96+
*
97+
* @param _definitions The map of definitions to add to.
98+
* @return The schema type.
99+
*/
100+
static schema::Type to_schema(
101+
std::map<std::string, schema::Type>* _definitions) {
102+
return Parser<R, W, VariantType, ProcessorsType>::to_schema(_definitions);
103+
}
104+
};
105+
106+
#endif // __cpp_lib_expected
107+
108+
template <class T>
109+
constexpr bool is_expected_v = is_expected<std::remove_cvref_t<T>>::value;
110+
111+
} // namespace rfl::parsing
112+
113+
#endif

0 commit comments

Comments
 (0)