From cfe9fa31baaa2b8577c6ea15f0621afe35996ccf Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Mon, 24 Aug 2026 20:45:40 +0200 Subject: [PATCH] Added support for std::expected --- README.md | 80 +++++++++++++---- docs/docs-readme.md | 2 + docs/expected.md | 108 +++++++++++++++++++++++ docs/result.md | 3 + docs/standard_containers.md | 4 +- include/rfl/bson/write.hpp | 9 +- include/rfl/parsing/ParserExpected.hpp | 113 +++++++++++++++++++++++++ include/rfl/parsing/Parser_default.hpp | 10 +++ mkdocs.yaml | 1 + tests/generic/test_expected.cpp | 41 +++++++++ tests/json/test_expected.cpp | 49 +++++++++++ 11 files changed, 399 insertions(+), 21 deletions(-) create mode 100644 docs/expected.md create mode 100644 include/rfl/parsing/ParserExpected.hpp create mode 100644 tests/generic/test_expected.cpp create mode 100644 tests/json/test_expected.cpp diff --git a/README.md b/README.md index cfa61dbe6..72859a13f 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 +#include + +// A success value is serialized like the value itself: +const std::expected 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 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>(json_string).value(); +const auto no_age2 = + rfl::json::read>(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> 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` and +`std::expected` 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. @@ -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` @@ -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 @@ -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. diff --git a/docs/docs-readme.md b/docs/docs-readme.md index c98e2466c..d33777e2a 100644 --- a/docs/docs-readme.md +++ b/docs/docs-readme.md @@ -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. diff --git a/docs/expected.md b/docs/expected.md new file mode 100644 index 000000000..9e8dc7059 --- /dev/null +++ b/docs/expected.md @@ -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 `` 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` 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 +#include + +const std::expected ok = 42; +const std::string ok_json = rfl::json::write(ok); +// -> 42 + +const std::expected 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>(ok_json).value(); +const auto err2 = rfl::json::read>(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 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> 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 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` 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` 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` 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` an alias for `std::expected`. This is a separate feature from the one described in this section, but the two can be combined. diff --git a/docs/result.md b/docs/result.md index 94bc930de..567206706 100644 --- a/docs/result.md +++ b/docs/result.md @@ -152,3 +152,6 @@ const auto embellish_error = [&](const Error& _e) -> rfl::Result { return Parser::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. diff --git a/docs/standard_containers.md b/docs/standard_containers.md index b41f6d4d0..0e0deca1c 100644 --- a/docs/standard_containers.md +++ b/docs/standard_containers.md @@ -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. diff --git a/include/rfl/bson/write.hpp b/include/rfl/bson/write.hpp index 9f9374d27..453c58fa5 100644 --- a/include/rfl/bson/write.hpp +++ b/include/rfl/bson/write.hpp @@ -53,10 +53,11 @@ Result> 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> { + bson_free(buf); + return error(_err.what()); + }); } /// Returns BSON bytes representation of the object. diff --git a/include/rfl/parsing/ParserExpected.hpp b/include/rfl/parsing/ParserExpected.hpp new file mode 100644 index 000000000..6d5cdc2c0 --- /dev/null +++ b/include/rfl/parsing/ParserExpected.hpp @@ -0,0 +1,113 @@ +#ifndef RFL_PARSING_PARSER_EXPECTED_HPP_ +#define RFL_PARSING_PARSER_EXPECTED_HPP_ + +#include +#include + +#include "../Field.hpp" +#include "../NamedTuple.hpp" +#include "../Result.hpp" +#include "../Variant.hpp" +#include "Parser_base.hpp" +#include "schema/Type.hpp" + +#if __has_include() +#include +#endif + +namespace rfl::parsing { + +template +struct is_expected : std::false_type {}; + +/// @brief Primary declaration; defined below if std::expected is available. +template +struct ParserExpected; + +#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L + +template +struct is_expected> : std::true_type {}; + +/** + * @brief Parser specialization for std::expected. + * + * A std::expected is serialized as an rfl::Variant: the success + * value and the error are treated as the two alternatives of a variant. + */ +template +struct ParserExpected { + using T = std::remove_cvref_t; + + using ValueType = typename T::value_type; + + using ErrorType = typename T::error_type; + + using WrappedErrorType = NamedTuple>; + + using VariantType = rfl::Variant; + + using InputVarType = typename R::InputVarType; + + static_assert(!std::is_void_v, + "std::expected 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 read(const R& _r, const InputVarType& _var) noexcept { + const auto to_expected = [](auto&& _alternative) -> T { + using AltType = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return T(std::unexpected( + std::forward(_alternative.template get<"error">()))); + } else { + return T(std::forward(_alternative)); + } + }; + return Parser::read(_r, _var).transform( + [&](auto&& _variant) -> T { + return std::forward(_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 + 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::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* _definitions) { + return Parser::to_schema(_definitions); + } +}; + +#endif // __cpp_lib_expected + +template +constexpr bool is_expected_v = is_expected>::value; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index b9a7f1a70..ad50b3e2b 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -34,6 +34,7 @@ #include "ParserDefaultVal.hpp" #include "ParserDuration.hpp" #include "ParserEnum.hpp" +#include "ParserExpected.hpp" #include "ParserFilepath.hpp" #include "ParserOptional.hpp" #include "ParserPair.hpp" @@ -138,6 +139,9 @@ struct Parser { } else if constexpr (is_result_v) { return ParserResult::read(_r, _var); + } else if constexpr (is_expected_v) { + return ParserExpected::read(_r, _var); + } else if constexpr (is_duration_v) { using U = std::remove_cvref_t; return ParserDuration) { ParserResult::write(_w, _var, _parent); + } else if constexpr (is_expected_v) { + ParserExpected::write(_w, _var, _parent); + } else if constexpr (is_variant_v) { ParserVariant::write(_w, _var, _parent); @@ -489,6 +496,9 @@ struct Parser { } else if constexpr (is_result_v) { return ParserResult::to_schema(_definitions); + } else if constexpr (is_expected_v) { + return ParserExpected::to_schema(_definitions); + } else if constexpr (is_duration_v) { return ParserDuration::to_schema(_definitions); diff --git a/mkdocs.yaml b/mkdocs.yaml index 04e4de8ce..306a38137 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -131,6 +131,7 @@ nav: - rfl::Commented: commented.md - rfl::Result: result.md - Standard containers: standard_containers.md + - std::expected: expected.md - C arrays and inheritance: c_arrays_and_inheritance.md - rfl::Bytestring: bytestring.md - rfl::Binary, rfl::Hex and rfl::Oct: number_systems.md diff --git a/tests/generic/test_expected.cpp b/tests/generic/test_expected.cpp new file mode 100644 index 000000000..15d4814e2 --- /dev/null +++ b/tests/generic/test_expected.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include + +#include "write_and_read.hpp" + +#if __has_include() +#include +#endif + +#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L + +namespace test_expected { + +TEST(generic, test_expected_success) { + const std::expected value = "hello"; + write_and_read(value); +} + +TEST(generic, test_expected_error) { + const std::expected value = std::unexpected(42); + write_and_read(value); +} + +struct Person { + std::string name; + std::expected age; +}; + +TEST(generic, test_expected_in_struct) { + const Person homer = {.name = "Homer", .age = 42}; + write_and_read(homer); + + const Person maggie = {.name = "Maggie", .age = std::unexpected("too young")}; + write_and_read(maggie); +} + +} // namespace test_expected + +#endif // __cpp_lib_expected diff --git a/tests/json/test_expected.cpp b/tests/json/test_expected.cpp new file mode 100644 index 000000000..7d9bc39ff --- /dev/null +++ b/tests/json/test_expected.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +#if __has_include() +#include +#endif + +#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L + +namespace test_expected { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + std::vector> ages; +}; + +TEST(json, test_expected_success) { + const std::expected value = 42; + write_and_read(value, "42"); +} + +TEST(json, test_expected_error) { + const std::expected value = + std::unexpected("Something went wrong."); + write_and_read(value, R"({"error":"Something went wrong."})"); +} + +TEST(json, test_expected_in_struct) { + const Person homer = {.first_name = "Homer", + .ages = std::vector>( + {42, std::unexpected("unknown age")})}; + write_and_read( + homer, R"({"firstName":"Homer","ages":[42,{"error":"unknown age"}]})"); +} + +TEST(json, test_expected_struct_value) { + const std::expected value = + Person{.first_name = "Bart", + .ages = std::vector>({10})}; + write_and_read(value, R"({"firstName":"Bart","ages":[10]})"); +} + +} // namespace test_expected + +#endif // __cpp_lib_expected