diff --git a/docs/enums.md b/docs/enums.md index edd1530b..2c6ad6dd 100644 --- a/docs/enums.md +++ b/docs/enums.md @@ -1,6 +1,8 @@ # Enums -reflect-cpp supports scoped enumerations. +reflect-cpp supports scoped enumerations. They can either come in the form of normal enumerations or flag enums. + +## Normal enumerations Example: @@ -38,7 +40,7 @@ enum struct Color2 { red, green, blue, yellow }; enum Color3 { red, green, blue, yellow }; ``` -2. You cannot have more than 100 values and if you explicitly assign values, they must be between 0 and 99. +2. You cannot have more than 128 values and if you explicitly assign values, they must be between 0 and 127. ```cpp /// OK @@ -50,6 +52,97 @@ enum struct Color2 { red, green, blue = 50, yellow }; /// unsupported - red is a negative value enum Color3 { red = -10, green, blue, yellow }; -/// unsupported - red is greater than 99 +/// unsupported - red is greater than 127 enum Color3 { red = 200, green, blue, yellow }; -``` \ No newline at end of file +``` + +## Flag enums + +Sometimes the enumerations are not mutually exclusive - sticking with the metaphor of colors, it is perfectly +possible for things to have more than one color. C++ programmers sometimes like to model this using a flag enum. + +Flag enums work as follows: + +1. The bitwise OR operator must be defined on them. If an enum has the bitwise OR operator defined on it, it will be treated as a flag enum. +2. The most important values of the enum must be 1 or 2^N, N being a positive integer (in other words 1,2,4,8,16,32,...). + +Example: + +```cpp +// The important colors must be 1 or 2^N +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +// The bitwise OR operator must be defined - this is how reflect-cpp knows that +// this is a flag enum. +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} +``` + +In this particular example, the important colors red, green, blue and yellow are all in the form of 2^N. +Other colors are ok as well, if they are expressed as combinations of the 2^N-colors. + +When something is a flag enum, then you can also do this: + +```cpp +const auto circle = + Circle{.radius = 2.0, .color = Color::blue | Color::green}; +``` + +Which will then be represented as follows: + +```json +{"radius":2.0,"color":"blue|green"} +``` + +Using orange is fine as well: + +```cpp +const auto circle = + Circle{.radius = 2.0, .color = Color::orange}; +``` + +But it will be represented in terms of the base colors (which are in the form 2^N): + +```json +{"radius":2.0,"color":"red|yellow"} +``` + +You can also combine orange with another color: + +```cpp +const auto circle = + Circle{.radius = 2.0, .color = Color::blue | Color::orange}; +``` + +Which will result in this: + +```json +{"radius":2.0,"color":"red|blue|yellow"} +``` + +## What happens if an enum cannot be matched? + +If an enum cannot be matched, it will be written as an integer. This works for both normal enums as well as flag enums. + +For instance, if you use the `Color` flag enum as described in the previous section, you can do something like this: + +```cpp +// Enums are not supposed to be used like this, but it is defined behavior for scoped enumerations. +const auto circle = Circle{.radius = 2.0, .color = static_cast(10000)}; +``` + +This will be represented as follows: + +```json +{"radius":2.0,"color":"16|red|green|blue|8192"} +``` + +This works, because 16 + 256 + 512 + 1024 + 8192 = 10000. Flag enums are *always* represented in terms of 2^N-numbers. + diff --git a/include/rfl/internal/enums/LiteralConverter.hpp b/include/rfl/internal/enums/LiteralConverter.hpp deleted file mode 100644 index 32b6be2a..00000000 --- a/include/rfl/internal/enums/LiteralConverter.hpp +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef RFL_INTERNAL_ENUMS_LITERAL_CONVERTER_HPP_ -#define RFL_INTERNAL_ENUMS_LITERAL_CONVERTER_HPP_ - -#include -#include -#include -#include -#include - -#include "rfl/Result.hpp" -#include "rfl/internal/enums/get_enum_names.hpp" -#include "rfl/type_name_t.hpp" - -namespace rfl { -namespace internal { -namespace enums { - -template -struct LiteralConverter { - static constexpr auto names_ = get_enum_names(); - - using NamesLiteral = typename decltype(names_)::Literal; - - static Result enum_to_literal(const EnumType _enum) { - for (size_t i = 0; i < names_.size; ++i) { - if (names_.enums_[i] == _enum) { - return NamesLiteral::from_value( - static_cast(i)); - } - } - return Error( - "Something went wrong when parsing the enum type '" + - type_name_t().str() + - "'. An integer representation of the enum type is " + - std::to_string(static_cast>(_enum)) + - ". Is it possible that some of the values are negative or " - "greater than 99? This is unsupported (as is explicitly " - "stated in the documentation)."); - } - - static Result enum_to_string(const EnumType _enum) { - const auto to_str = [](const auto _l) { return _l.str(); }; - return enum_to_literal(_enum).transform(to_str); - } - - static EnumType literal_to_enum(const NamesLiteral _lit) { - static_assert(names_.size != 0, "This does not work for empty enums."); - return names_.enums_[_lit.value()]; - } - - static Result string_to_enum(const std::string& _str) { - return NamesLiteral::from_string(_str).transform(literal_to_enum); - } -}; - -} // namespace enums -} // namespace internal -} // namespace rfl - -#endif diff --git a/include/rfl/internal/enums/Names.hpp b/include/rfl/internal/enums/Names.hpp index 4f66e42c..0cfcd5ce 100644 --- a/include/rfl/internal/enums/Names.hpp +++ b/include/rfl/internal/enums/Names.hpp @@ -8,12 +8,13 @@ #include #include "rfl/Literal.hpp" +#include "rfl/define_literal.hpp" namespace rfl { namespace internal { namespace enums { -template +template struct Names { /// Contains a collection of enums as compile-time strings. using Literal = LiteralType; @@ -22,10 +23,17 @@ struct Names { constexpr static size_t size = N; /// A list of all the possible enums - std::array enums_; + constexpr static std::array enums_ = + std::array{_enums...}; static_assert(N == 0 || LiteralType::size() == N, "Size of literal and enum do not match."); + + template + using AddOneType = std::conditional_t< + N == 0, Names, + Names, N + 1, + _enums..., _new_enum>>; }; } // namespace enums diff --git a/include/rfl/internal/enums/StringConverter.hpp b/include/rfl/internal/enums/StringConverter.hpp new file mode 100644 index 00000000..eb017719 --- /dev/null +++ b/include/rfl/internal/enums/StringConverter.hpp @@ -0,0 +1,132 @@ +#ifndef RFL_INTERNAL_ENUMS_STRINGCONVERTER_HPP_ +#define RFL_INTERNAL_ENUMS_STRINGCONVERTER_HPP_ + +#include +#include +#include +#include +#include + +#include "rfl/Result.hpp" +#include "rfl/internal/enums/get_enum_names.hpp" +#include "rfl/internal/enums/is_flag_enum.hpp" +#include "rfl/internal/strings/join.hpp" +#include "rfl/internal/strings/split.hpp" +#include "rfl/type_name_t.hpp" + +namespace rfl { +namespace internal { +namespace enums { + +template +class StringConverter { + private: + static constexpr bool is_flag_enum_ = is_flag_enum; + + static constexpr auto names_ = get_enum_names(); + + using NamesLiteral = typename decltype(names_)::Literal; + + public: + /// Transform an enum to a matching string. + static std::string enum_to_string(const EnumType _enum) { + if constexpr (is_flag_enum_) { + return flag_enum_to_string(_enum); + } else { + return enum_to_single_string(_enum); + } + } + + /// Transforms a string to the matching enum. + static Result string_to_enum(const std::string& _str) { + static_assert(names_.size != 0, + "No enum could be identified. Please choose enum values " + "between 0 to 127 or for flag enums choose 1,2,4,8,16,..."); + if constexpr (is_flag_enum_) { + return string_to_flag_enum(_str); + } else { + return single_string_to_enum(_str); + } + } + + private: + /// Iterates through the enum bit by bit and matches it against the flags. + static std::string flag_enum_to_string(const EnumType _e) { + using T = std::underlying_type_t; + auto val = static_cast(_e); + int i = 0; + std::vector flags; + while (val != 0) { + const auto bit = val & static_cast(1); + if (bit == 1) { + auto str = enum_to_single_string( + static_cast(static_cast(1) << i)); + flags.emplace_back(std::move(str)); + } + ++i; + val >>= 1; + } + return strings::join("|", flags); + } + + /// This assumes that _enum can be exactly matched to one of the names and + /// does not have to be combined using |. + static std::string enum_to_single_string(const EnumType _enum) { + const auto to_str = [](const auto _l) { return _l.str(); }; + + for (size_t i = 0; i < names_.size; ++i) { + if (names_.enums_[i] == _enum) { + return NamesLiteral::from_value( + static_cast(i)) + .transform(to_str) + .value(); + } + } + + return std::to_string(static_cast>(_enum)); + } + + /// Finds the enum matching the literal. + static EnumType literal_to_enum(const NamesLiteral _lit) noexcept { + return names_.enums_[_lit.value()]; + } + + /// This assumes that _enum can be exactly matched to one of the names and + /// does not have to be combined using |. + static Result single_string_to_enum(const std::string& _str) { + const auto r = NamesLiteral::from_string(_str).transform(literal_to_enum); + if (r) { + return r; + } else { + try { + return static_cast(std::stoi(_str)); + } catch (std::exception& exp) { + return Error(exp.what()); + } + } + } + + /// Only relevant if this is a flag enum - combines the different matches + /// using |. + static Result string_to_flag_enum( + const std::string& _str) noexcept { + using T = std::underlying_type_t; + const auto split = strings::split(_str, "|"); + auto res = static_cast(0); + for (const auto& s : split) { + const auto r = single_string_to_enum(s); + if (r) { + res |= static_cast(*r); + } else { + return r; + } + } + return static_cast(res); + } +}; + +} // namespace enums +} // namespace internal +} // namespace rfl + +#endif diff --git a/include/rfl/internal/enums/get_enum_names.hpp b/include/rfl/internal/enums/get_enum_names.hpp index e932a8ff..4ad150cc 100644 --- a/include/rfl/internal/enums/get_enum_names.hpp +++ b/include/rfl/internal/enums/get_enum_names.hpp @@ -8,6 +8,7 @@ #include "rfl/Literal.hpp" #include "rfl/define_literal.hpp" #include "rfl/internal/enums/Names.hpp" +#include "rfl/internal/enums/is_scoped_enum.hpp" #include "rfl/internal/remove_namespaces.hpp" // https://en.cppreference.com/w/cpp/language/static_cast: @@ -67,53 +68,76 @@ consteval auto get_enum_name() { return to_str_lit(std::make_index_sequence{}); } -template -constexpr auto start_value = - Names, 0>{.enums_ = std::array{}}; +template +consteval T calc_greatest_power_of_two() { + if constexpr (std::is_signed_v) { + return static_cast(1) << (sizeof(T) * 8 - 2); + } else { + return static_cast(1) << (sizeof(T) * 8 - 1); + } +} -template , int _i = 0> -consteval auto get_enum_names() { - static_assert( - std::is_enum_v && - !std::is_convertible_v>, - "You must use scoped enums (using class or struct) for the " - "parsing to work!"); +template +consteval T get_max() { + if constexpr (_is_flag) { + return calc_greatest_power_of_two(); + } else { + return std::numeric_limits::max() > 127 ? static_cast(127) + : std::numeric_limits::max(); + } +} - static_assert(std::is_integral_v>, - "The underlying type of any Enum must be integral!"); +template +consteval T calc_j() { + if constexpr (_is_flag) { + return static_cast(1) << _i; + } else { + return static_cast(_i); + } +} + +template +consteval auto get_enum_names_impl() { + using T = std::underlying_type_t; - constexpr auto max = - std::numeric_limits>::max(); + constexpr T j = calc_j(); - if constexpr (_i == 100 || _i > max) { - return _names; + constexpr auto name = get_enum_name(j)>(); + + if constexpr (std::get<0>(name.arr_) == '(') { + if constexpr (j == _max) { + return NamesType{}; + } else { + return get_enum_names_impl(); + } } else { - constexpr auto name = get_enum_name(_i)>(); - if constexpr (std::get<0>(name.arr_) == '(') { - return get_enum_names(); + using NewNames = typename NamesType::template AddOneType< + Literal()>, static_cast(j)>; + + if constexpr (j == _max) { + return NewNames{}; } else { - const auto update_enums = [&](std::index_sequence) { - return std::array{ - _names.enums_[Ns]..., static_cast(_i)}; - }; - - using NewNames = std::conditional_t< - decltype(_names)::size == 0, - Names()>, 1>, - Names()>>, - decltype(_names)::size + 1>>; - - constexpr auto new_names = - NewNames{.enums_ = update_enums( - std::make_index_sequence{})}; - - return get_enum_names(); + return get_enum_names_impl(); } } } +template +consteval auto get_enum_names() { + static_assert(is_scoped_enum, + "You must use scoped enums (using class or struct) for the " + "parsing to work!"); + + static_assert(std::is_integral_v>, + "The underlying type of any Enum must be integral!"); + + constexpr auto max = get_max, _is_flag>(); + + using EmptyNames = Names, 0>; + + return get_enum_names_impl(); +} + } // namespace enums } // namespace internal } // namespace rfl diff --git a/include/rfl/internal/enums/is_flag_enum.hpp b/include/rfl/internal/enums/is_flag_enum.hpp new file mode 100644 index 00000000..8c81f9ed --- /dev/null +++ b/include/rfl/internal/enums/is_flag_enum.hpp @@ -0,0 +1,22 @@ +#ifndef RFL_INTERNAL_ENUMS_IS_FLAG_ENUM_HPP_ +#define RFL_INTERNAL_ENUMS_IS_FLAG_ENUM_HPP_ + +#include + +#include "rfl/internal/enums/is_scoped_enum.hpp" + +namespace rfl { +namespace internal { +namespace enums { + +template +concept is_flag_enum = is_scoped_enum && + requires(EnumType e1, EnumType e2) { + { e1 | e2 } -> std::same_as; +}; + +} // namespace enums +} // namespace internal +} // namespace rfl + +#endif diff --git a/include/rfl/internal/enums/is_scoped_enum.hpp b/include/rfl/internal/enums/is_scoped_enum.hpp new file mode 100644 index 00000000..958aca8a --- /dev/null +++ b/include/rfl/internal/enums/is_scoped_enum.hpp @@ -0,0 +1,19 @@ +#ifndef RFL_INTERNAL_ENUMS_IS_SCOPED_ENUM_HPP_ +#define RFL_INTERNAL_ENUMS_IS_SCOPED_ENUM_HPP_ + +#include +#include + +namespace rfl { +namespace internal { +namespace enums { + +template +concept is_scoped_enum = std::is_enum_v && + !std::is_convertible_v>; + +} // namespace enums +} // namespace internal +} // namespace rfl + +#endif diff --git a/include/rfl/internal/strings/join.hpp b/include/rfl/internal/strings/join.hpp new file mode 100644 index 00000000..c562e773 --- /dev/null +++ b/include/rfl/internal/strings/join.hpp @@ -0,0 +1,28 @@ +#ifndef RFL_INTERNAL_STRINGS_JOIN_HPP_ +#define RFL_INTERNAL_STRINGS_JOIN_HPP_ + +#include +#include + +namespace rfl { +namespace internal { +namespace strings { + +/// Joins a string using the delimiter +inline std::string join(const std::string& _delimiter, + const std::vector& _strings) { + if (_strings.size() == 0) { + return ""; + } + auto res = _strings[0]; + for (size_t i = 1; i < _strings.size(); ++i) { + res += _delimiter + _strings[i]; + } + return res; +} + +} // namespace strings +} // namespace internal +} // namespace rfl + +#endif diff --git a/include/rfl/internal/strings/split.hpp b/include/rfl/internal/strings/split.hpp new file mode 100644 index 00000000..6bf29027 --- /dev/null +++ b/include/rfl/internal/strings/split.hpp @@ -0,0 +1,29 @@ +#ifndef RFL_INTERNAL_STRINGS_SPLIT_HPP_ +#define RFL_INTERNAL_STRINGS_SPLIT_HPP_ + +#include +#include + +namespace rfl { +namespace internal { +namespace strings { + +/// Splits a string alongside the delimiter +inline std::vector split(const std::string& _str, + const std::string& _delimiter) { + auto str = _str; + size_t pos = 0; + std::vector result; + while ((pos = str.find(_delimiter)) != std::string::npos) { + result.emplace_back(str.substr(0, pos)); + str.erase(0, pos + _delimiter.length()); + } + result.emplace_back(std::move(str)); + return result; +} + +} // namespace strings +} // namespace internal +} // namespace rfl + +#endif diff --git a/include/rfl/parsing/Parser.hpp b/include/rfl/parsing/Parser.hpp index 23ffd226..af849087 100644 --- a/include/rfl/parsing/Parser.hpp +++ b/include/rfl/parsing/Parser.hpp @@ -35,7 +35,7 @@ #include "rfl/internal/Memoization.hpp" #include "rfl/internal/StringLiteral.hpp" #include "rfl/internal/all_fields.hpp" -#include "rfl/internal/enums/LiteralConverter.hpp" +#include "rfl/internal/enums/StringConverter.hpp" #include "rfl/internal/flattened_ptr_tuple_t.hpp" #include "rfl/internal/flattened_tuple_t.hpp" #include "rfl/internal/get_field_names.hpp" @@ -106,9 +106,9 @@ struct Parser { }; return Parser::read(_r, _var).and_then(to_struct); } else if constexpr (std::is_enum_v) { - using LiteralConverter = internal::enums::LiteralConverter; + using StringConverter = internal::enums::StringConverter; return _r.template to_basic_type(_var).and_then( - LiteralConverter::string_to_enum); + StringConverter::string_to_enum); } else if constexpr (internal::is_basic_type_v) { return _r.template to_basic_type>(_var); } else { @@ -136,12 +136,8 @@ struct Parser { using PtrNamedTupleType = std::decay_t; return Parser::write(_w, ptr_named_tuple); } else if constexpr (std::is_enum_v) { - using LiteralConverter = internal::enums::LiteralConverter; - const auto handle_error = [](const auto& _err) -> std::string { - return _err.what(); - }; - const auto str = - LiteralConverter::enum_to_string(_var).or_else(handle_error).value(); + using StringConverter = internal::enums::StringConverter; + const auto str = StringConverter::enum_to_string(_var); return _w.from_basic_type(str); } else if constexpr (internal::is_basic_type_v) { return _w.from_basic_type(_var); diff --git a/tests/json/CMakeLists.txt b/tests/json/CMakeLists.txt index ccaac37b..5eef67a8 100644 --- a/tests/json/CMakeLists.txt +++ b/tests/json/CMakeLists.txt @@ -30,6 +30,8 @@ add_executable( "test_enum.cpp" "test_error_messages.cpp" "test_field_variant.cpp" + "test_flag_enum.cpp" + "test_flag_enum_with_int.cpp" "test_flatten.cpp" "test_flatten_anonymous.cpp" "test_forward_list.cpp" diff --git a/tests/json/test_flag_enum.cpp b/tests/json/test_flag_enum.cpp new file mode 100644 index 00000000..5f8e61a7 --- /dev/null +++ b/tests/json/test_flag_enum.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "test_enum.hpp" +#include "write_and_read.hpp" + +namespace test_flag_enum { + +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} + +struct Circle { + float radius; + Color color; +}; + +void test() { + std::cout << std::source_location::current().function_name() << std::endl; + + const auto circle = + Circle{.radius = 2.0, .color = Color::blue | Color::orange}; + + write_and_read(circle, R"({"radius":2.0,"color":"red|blue|yellow"})"); +} + +} // namespace test_flag_enum diff --git a/tests/json/test_flag_enum.hpp b/tests/json/test_flag_enum.hpp new file mode 100644 index 00000000..2f4dc7a0 --- /dev/null +++ b/tests/json/test_flag_enum.hpp @@ -0,0 +1,4 @@ +namespace test_flag_enum { +void test(); +} + diff --git a/tests/json/test_flag_enum_with_int.cpp b/tests/json/test_flag_enum_with_int.cpp new file mode 100644 index 00000000..9c159289 --- /dev/null +++ b/tests/json/test_flag_enum_with_int.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "test_enum.hpp" +#include "write_and_read.hpp" + +namespace test_flag_enum_with_int { + +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} + +struct Circle { + float radius; + Color color; +}; + +void test() { + std::cout << std::source_location::current().function_name() << std::endl; + + const auto circle = Circle{.radius = 2.0, .color = static_cast(10000)}; + + write_and_read(circle, R"({"radius":2.0,"color":"16|red|green|blue|8192"})"); +} + +} // namespace test_flag_enum_with_int diff --git a/tests/json/test_flag_enum_with_int.hpp b/tests/json/test_flag_enum_with_int.hpp new file mode 100644 index 00000000..a7512b60 --- /dev/null +++ b/tests/json/test_flag_enum_with_int.hpp @@ -0,0 +1,4 @@ +namespace test_flag_enum_with_int { +void test(); +} + diff --git a/tests/json/tests.cpp b/tests/json/tests.cpp index 6ead366c..7a113ab9 100644 --- a/tests/json/tests.cpp +++ b/tests/json/tests.cpp @@ -19,6 +19,8 @@ #include "test_enum.hpp" #include "test_error_messages.hpp" #include "test_field_variant.hpp" +#include "test_flag_enum.hpp" +#include "test_flag_enum_with_int.hpp" #include "test_flatten.hpp" #include "test_flatten_anonymous.hpp" #include "test_forward_list.hpp" @@ -67,6 +69,8 @@ int main() { test_unique_ptr2::test(); test_literal::test(); test_enum::test(); + test_flag_enum::test(); + test_flag_enum_with_int::test(); test_variant::test(); test_tagged_union::test(); test_tagged_union2::test();