diff --git a/include/reflection-cpp/reflection.hpp b/include/reflection-cpp/reflection.hpp index e32f2b8..bb00305 100644 --- a/include/reflection-cpp/reflection.hpp +++ b/include/reflection-cpp/reflection.hpp @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include #include #include @@ -20,6 +22,14 @@ #define REFLECTION_PRETTY_FUNCTION __FUNCSIG__ #endif +// Structured binding packs (P1061) give direct access to all members of an aggregate at once. +// Clang provides them as an extension before C++26, and warns about their use there. +#if defined(__cpp_structured_bindings) && __cpp_structured_bindings >= 202411L + #define REFLECTION_HAS_STRUCTURED_BINDING_PACKS 1 +#else + #define REFLECTION_HAS_STRUCTURED_BINDING_PACKS 0 +#endif + namespace Reflection { @@ -127,10 +137,8 @@ constexpr size_t MaxReflectionMemerCount = 150; namespace detail { -#if defined(__cpp_structured_bindings) && __cpp_structured_bindings >= 202411L +#if REFLECTION_HAS_STRUCTURED_BINDING_PACKS // A structured binding pack yields the member count directly, at constant compile-time cost. - // - // Clang provides structured binding packs as an extension before C++26, and warns about their use there. #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wc++26-extensions" @@ -248,10 +256,23 @@ elisp functions to fill the ToTuple function **/ +#if REFLECTION_HAS_STRUCTURED_BINDING_PACKS && defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wc++26-extensions" +#endif + +/// Gets a tuple of references to the members of an aggregate. +/// +/// The members are never copied: the returned tuple refers into @p t, so it must not outlive it. +/// In particular, the result for a temporary is only valid until the end of the full expression. template > requires(N <= MaxReflectionMemerCount) constexpr decltype(auto) ToTuple(T&& t) noexcept { +#if REFLECTION_HAS_STRUCTURED_BINDING_PACKS + auto&& [... members] = std::forward(t); + return std::tie(members...); +#else if constexpr (N == 0) return std::tuple {}; // clang-format off @@ -1007,8 +1028,16 @@ constexpr decltype(auto) ToTuple(T&& t) noexcept } // clang-format on +#endif } +#if REFLECTION_HAS_STRUCTURED_BINDING_PACKS && defined(__clang__) + #pragma clang diagnostic pop +#endif + +/// Gets a reference to the member at index I of an aggregate. +/// +/// The returned reference refers into @p t, so it must not outlive it. template constexpr decltype(auto) GetMemberAt(T&& t) { @@ -1017,7 +1046,7 @@ constexpr decltype(auto) GetMemberAt(T&& t) /// Represents the type of the member at index I of type Object template -using MemberTypeOf = std::remove_cvref_t(ToTuple(Object {})))>; +using MemberTypeOf = std::remove_cvref_t(ToTuple(std::declval())))>; template struct WrappedPointer final @@ -1096,14 +1125,24 @@ inline constexpr auto MemberNameOf = []() constexpr { template constexpr auto TypeNameOf = [] { constexpr std::string_view name = detail::MangledName(); - constexpr auto begin = name.find(detail::reflect_type::end); + constexpr auto begin = name.rfind(detail::reflect_type::end); constexpr auto tmp = name.substr(0, begin); #if defined(__GNUC__) || defined(__clang__) return tmp.substr(tmp.rfind(detail::reflect_type::begin) + detail::reflect_type::begin.size()); #else constexpr auto name_with_keyword = tmp.substr(tmp.rfind(detail::reflect_type::begin) + detail::reflect_type::begin.size()); - return name_with_keyword.substr(name_with_keyword.find(' ') + 1); + // MSVC prefixes user-defined types with their class-key, e.g. "struct Person". + // Types such as "unsigned int" contain a space as well, and must be left alone. + for (auto const keyword: { std::string_view { "struct " }, + std::string_view { "class " }, + std::string_view { "enum " }, + std::string_view { "union " } }) + { + if (name_with_keyword.starts_with(keyword)) + return name_with_keyword.substr(keyword.size()); + } + return name_with_keyword; #endif }(); @@ -1335,16 +1374,21 @@ constexpr void EnumerateMembers(Callable&& callable) } template - requires std::is_invocable_v> + requires(CountMembers == 0) || std::is_invocable_v> void CallOnMembers(Object& object, Callable&& callable) { EnumerateMembers(object, [&](T&& value) { callable(MemberNameOf, value); }); } +/// Calls a callable on each member of an object, with the index of the member and its type as template arguments. +/// +/// The type is the plain member type, without reference or cv-qualifiers, as in MemberTypeOf. template void CallOnMembersWithoutName(Object& object, Callable&& callable) { - EnumerateMembers(object, [&](T&& value) { callable.template operator()(value); }); + EnumerateMembers(object, [&](T&& value) { + callable.template operator()>(value); + }); } /// Folds over the members of a type without an object of it. @@ -1352,16 +1396,17 @@ void CallOnMembersWithoutName(Object& object, Callable&& callable) /// @param initialValue The initial value to fold with /// @param callable The callable to fold with. The parameters are the member name, /// the member's default value and the current result of the fold. +/// The current result is passed as an rvalue, so it is moved rather than copied. /// /// @return The result of the fold template constexpr ResultType FoldMembers(ResultType initialValue, Callable const& callable) { // clang-format off - ResultType result = initialValue; + ResultType result = std::move(initialValue); EnumerateMembers( [&]() { - result = callable.template operator()>(result); + result = callable.template operator()>(std::move(result)); } ); // clang-format on @@ -1374,56 +1419,134 @@ constexpr ResultType FoldMembers(ResultType initialValue, Callable const& callab /// @param initialValue The initial value to fold with /// @param callable The callable to fold with. The parameters are the member name, /// the member value and the current result of the fold. +/// The current result is passed as an rvalue, so it is moved rather than copied. /// /// @return The result of the fold template - requires std::same_as, ResultType>> + requires(CountMembers == 0) + || std::same_as, ResultType>> constexpr ResultType FoldMembers(Object& object, ResultType initialValue, Callable const& callable) { // clang-format off - ResultType result = initialValue; + ResultType result = std::move(initialValue); EnumerateMembers( object, [&](MemberType&& value) { - result = callable(MemberNameOf, value, result); + result = callable(MemberNameOf, value, std::move(result)); } ); return result; // clang-format on } -template -std::string Inspect(Object const& object) +namespace detail { - std::string str; - auto const onMember = [&str](Name&& name, Value&& value) { - auto const InspectValue = [&str](T&& arg) { - // clang-format off - if constexpr (std::is_convertible_v - || std::is_convertible_v - || std::is_convertible_v) // clang-format on - { - str += std::format("\"{}\"", arg); - } - else if constexpr (std::is_convertible_v) // use std::formattable when available + template + concept InspectableAsString = std::is_convertible_v || std::is_convertible_v + || std::is_convertible_v; + + // Stand-in for std::formattable, which is only available as of C++23. + template + concept InspectableByFormat = std::is_default_constructible_v, char>>; + + template + concept InspectableAsOptional = requires(T const& value) { + { value.has_value() } -> std::convertible_to; + *value; + }; + + template + concept InspectableAsRange = requires(T const& value) { + std::begin(value); + std::end(value); + }; + + template + void InspectMembers(std::string& str, Object const& object); + + template + void InspectValue(std::string& str, T const& value) + { + if constexpr (InspectableAsString) + { + if constexpr (std::is_pointer_v) { - str += std::format("{}", arg); + if (value == nullptr) + { + str += "null"; + return; + } } + std::format_to(std::back_inserter(str), "\"{}\"", value); + } + else if constexpr (std::is_enum_v) + { + // The unary plus promotes character-sized underlying types, to print them as numbers. + std::format_to(std::back_inserter(str), "{}", +static_cast>(value)); + } + else if constexpr (InspectableAsOptional) + { + if (value.has_value()) + InspectValue(str, *value); else + str += "nullopt"; + } + else if constexpr (InspectableAsRange) + { + str += '['; + auto first = true; + for (auto const& element: value) { - str += '{'; - str += Inspect(arg); - str += '}'; + if (!std::exchange(first, false)) + str += ", "; + InspectValue(str, element); } - }; - if (!str.empty()) - str += ' '; - str += name; - str += '='; - InspectValue(value); - }; + str += ']'; + } + else if constexpr (InspectableByFormat) + { + std::format_to(std::back_inserter(str), "{}", value); + } + else if constexpr (std::is_aggregate_v) + { + str += '{'; + InspectMembers(str, value); + str += '}'; + } + else + { + // A value we have no way of printing: show its type instead. + str += '<'; + str += TypeNameOf; + str += '>'; + } + } + + template + void InspectMembers(std::string& str, Object const& object) + { + auto first = true; + CallOnMembers(object, [&](std::string_view name, auto const& value) { + if (!std::exchange(first, false)) + str += ' '; + str += name; + str += '='; + InspectValue(str, value); + }); + } +} // namespace detail - CallOnMembers(object, onMember); +/// Creates a human readable representation of an object, listing its members as `name=value`. +/// +/// Strings are quoted, enums are shown by their numeric value, optionals by their value or `nullopt`, +/// ranges as `[a, b]`, and nested aggregates as `{name=value ...}`. A member of any other type that +/// cannot be formatted is shown as its type name in angle brackets. +template +std::string Inspect(Object const& object) +{ + std::string str; + detail::InspectMembers(str, object); return str; } @@ -1433,12 +1556,14 @@ std::string Inspect(std::vector const& objects) std::string str; for (auto const& object: objects) { - str += Inspect(object); + detail::InspectMembers(str, object); str += '\n'; } return str; } +/// Calls a callback for each member that differs between two objects, with the name of the member +/// and its value in both objects. Members that are not equality comparable are compared member-wise. template void CollectDifferences(const Object& lhs, const Object& rhs, Callback const& callback) { @@ -1457,6 +1582,12 @@ void CollectDifferences(const Object& lhs, const Object& rhs, Callback const& ca }); } +/// Calls a callback for each member that differs between two objects, with the index of the member +/// and its value in both objects. +/// +/// This overload is selected for every callback that can be called with an index as first argument. +/// That includes callbacks declaring it as `auto`: to receive the name of the member instead, declare +/// the first parameter as `std::string_view`. template requires std::same_as, MemberTypeOf<0, Object>>> diff --git a/test-reflection-cpp.cpp b/test-reflection-cpp.cpp index 2d42942..4925221 100644 --- a/test-reflection-cpp.cpp +++ b/test-reflection-cpp.cpp @@ -3,9 +3,13 @@ #include +#include +#include +#include #include #include #include +#include struct Person { @@ -118,6 +122,151 @@ TEST_CASE("CountMembers.NoDefaultCtor", "[reflection]") CHECK(names == std::vector { "a", "b", "c", "d" }); } +struct ComparableNoDefaultCtor +{ + ComparableNoDefaultCtor() = delete; + constexpr ComparableNoDefaultCtor(int value): + value { value } + { + } + int value; + bool operator==(ComparableNoDefaultCtor const&) const = default; +}; + +struct RecordWithNoDefaultCtor +{ + int id; + ComparableNoDefaultCtor payload; + std::string name; +}; + +TEST_CASE("NoDefaultCtor.all_apis", "[reflection]") +{ + static_assert(std::same_as, int>); + static_assert(std::same_as, ComparableNoDefaultCtor>); + static_assert(std::same_as, std::string>); + + auto const lhs = RecordWithNoDefaultCtor { .id = 1, .payload = ComparableNoDefaultCtor { 2 }, .name = "three" }; + auto const rhs = RecordWithNoDefaultCtor { .id = 1, .payload = ComparableNoDefaultCtor { 4 }, .name = "three" }; + + std::string names; + Reflection::CallOnMembers(lhs, [&](std::string_view name, auto const& /*value*/) { names += name; }); + CHECK(names == "idpayloadname"); + + size_t typeCount = 0; + Reflection::EnumerateMembers([&]() { ++typeCount; }); + CHECK(typeCount == 3); + + auto const memberCount = Reflection::FoldMembers( + lhs, size_t { 0 }, [](auto&& /*name*/, auto&& /*value*/, auto&& accum) { return accum + 1; }); + CHECK(memberCount == 3); + + std::string differences; + Reflection::CollectDifferences( + lhs, rhs, [&](std::string_view name, auto const& /*lhs*/, auto const& /*rhs*/) { differences += name; }); + CHECK(differences == "payload"); +} + +template +struct RequiredField +{ + constexpr RequiredField() = delete; + constexpr RequiredField(RequiredField const&) = default; + constexpr RequiredField(RequiredField&&) noexcept = default; + constexpr RequiredField& operator=(RequiredField const&) = default; + constexpr RequiredField& operator=(RequiredField&&) noexcept = default; + constexpr ~RequiredField() = default; + + template + requires(sizeof...(S) != 0) && std::constructible_from + constexpr RequiredField(S&&... args): + value { std::forward(args)... } + { + } + + bool operator==(RequiredField const&) const = default; + + T value; +}; + +struct RecordWithRequiredFields +{ + RequiredField id; + std::optional age; + RequiredField name; + int visits; +}; + +TEST_CASE("NoDefaultCtor.required_fields", "[reflection]") +{ + static_assert(Reflection::CountMembers == 4); + static_assert(std::same_as, RequiredField>); + static_assert(std::same_as, std::optional>); + static_assert(std::same_as, RequiredField>); + static_assert(std::same_as, int>); + + static_assert(Reflection::MemberNameOf<0, RecordWithRequiredFields> == "id"); + static_assert(Reflection::MemberNameOf<2, RecordWithRequiredFields> == "name"); + static_assert(Reflection::MemberNames.size() == 4); + static_assert(Reflection::NameOf<&RecordWithRequiredFields::name> == "name"); + static_assert(Reflection::MemberIndexOf<&RecordWithRequiredFields::name> == 2); + static_assert(Reflection::MemberIndexOf<&RecordWithRequiredFields::visits> == 3); + + auto record = RecordWithRequiredFields { .id = 7, .age = std::nullopt, .name = "Jane", .visits = 3 }; + CHECK(Reflection::GetMemberAt<0>(record).value == 7); + CHECK(Reflection::GetMemberAt<2>(record).value == "Jane"); + + // members are handed out by reference, also for writing + Reflection::GetMemberAt<3>(record) = 4; + CHECK(record.visits == 4); + + std::string names; + Reflection::EnumerateMembers>(record, [&](auto const& /*value*/) { + names += Reflection::MemberNameOf; + }); + CHECK(names == "idname"); + + auto other = record; + other.name = RequiredField { "John" }; + std::vector differences; + Reflection::CollectDifferences( + record, other, [&](size_t index, auto const& /*lhs*/, auto const& /*rhs*/) { differences.push_back(index); }); + CHECK(differences == std::vector { 2 }); +} + +struct EmptyRecord +{ +}; + +TEST_CASE("empty record", "[reflection]") +{ + static_assert(Reflection::CountMembers == 0); + + auto const empty = EmptyRecord {}; + CHECK(Reflection::Inspect(empty).empty()); + + size_t calls = 0; + Reflection::CallOnMembers(empty, [&](auto&& /*name*/, auto&& /*value*/) { ++calls; }); + CHECK(calls == 0); + + auto const folded = + Reflection::FoldMembers(empty, 42, [](auto&& /*name*/, auto&& /*value*/, auto&& accum) { return accum; }); + CHECK(folded == 42); +} + +TEST_CASE("TypeNameOf.fundamental", "[reflection]") +{ + CHECK(Reflection::TypeNameOf == "unsigned int"); + CHECK(Reflection::TypeNameOf == "double"); +} + +TEST_CASE("TypeNameOf.array", "[reflection]") +{ + // The spelling of array types differs between compilers ("int[3]" vs "int [3]") + CHECK(Reflection::TypeNameOf.starts_with("int")); + CHECK(Reflection::TypeNameOf.ends_with("[3]")); +} + TEST_CASE("single value record", "[reflection]") { static_assert(Reflection::CountMembers == 1); @@ -240,6 +389,61 @@ TEST_CASE("CallOnMembers", "[reflection]") resultAnother += std::format("{}", value); }); CHECK(resultAnother == R"(John Doejohn@doe.com42)"); + + // The callable receives the plain member type, for const and non-const objects alike. + auto const constPerson = ps; + Reflection::CallOnMembersWithoutName(constPerson, [](FieldType const& /*value*/) { + static_assert(std::same_as>); + }); + Reflection::CallOnMembersWithoutName(ps, [](FieldType& /*value*/) { + static_assert(std::same_as>); + }); +} + +enum class Level : std::uint8_t +{ + Low = 1, + High = 2 +}; + +struct Opaque +{ + Opaque() = default; + Opaque(Opaque const&) = delete; + Opaque& operator=(Opaque const&) = delete; + std::mutex mutex; +}; + +struct InspectedRecord +{ + Color color; + Level level; + std::optional missing; + std::optional present; + std::vector numbers; + std::vector people; + char const* text; + char const* nullText; + Opaque opaque; +}; + +TEST_CASE("Inspect.member_kinds", "[reflection]") +{ + auto const record = InspectedRecord { + .color = Color::Blue, + .level = Level::High, + .missing = std::nullopt, + .present = "yes", + .numbers = { 1, 2, 3 }, + .people = { Person { .name = "John Doe", .email = "john@doe.com", .age = 42 } }, + .text = "text", + .nullText = nullptr, + .opaque = {}, + }; + + CHECK(Reflection::Inspect(record) + == R"(color=2 level=2 missing=nullopt present="yes" numbers=[1, 2, 3] )" + R"(people=[{name="John Doe" email="john@doe.com" age=42}] text="text" nullText=null opaque=)"); } TEST_CASE("FoldMembers.type", "[reflection]") @@ -268,6 +472,28 @@ TEST_CASE("FoldMembers.value", "[reflection]") CHECK(result == 6); } +struct MoveOnlyAccumulator +{ + MoveOnlyAccumulator() = default; + MoveOnlyAccumulator(MoveOnlyAccumulator const&) = delete; + MoveOnlyAccumulator& operator=(MoveOnlyAccumulator const&) = delete; + MoveOnlyAccumulator(MoveOnlyAccumulator&&) = default; + MoveOnlyAccumulator& operator=(MoveOnlyAccumulator&&) = default; + int count = 0; +}; + +TEST_CASE("FoldMembers.move_only_accumulator", "[reflection]") +{ + auto const s = S { .a = 1, .b = 2, .c = 3 }; + auto const result = Reflection::FoldMembers( + s, MoveOnlyAccumulator {}, [](auto&& /*name*/, auto&& /*value*/, MoveOnlyAccumulator accum) { + ++accum.count; + return accum; + }); + + CHECK(result.count == 3); +} + TEST_CASE("MemberTypeOf", "[reflection]") { static_assert(std::same_as, int>);