diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c480d36..ee6fed5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,6 @@ on: jobs: - # FIXME: We're having a linker error wrt. Catch2 on Windows with MSVC windows-msvc: name: "Windows - MSVC - C++${{ matrix.cxx_standard }}" runs-on: windows-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 9350f20..ce8a861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(reflection-cpp VERSION 0.4.0 LANGUAGES CXX) +project(reflection-cpp VERSION 0.5.0 LANGUAGES CXX) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/README.md b/README.md index b18a68f..b3ae781 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,94 @@ This is a C++ static reflection library. - Minimal to zero runtime overhead - Works with C++20 and later - Prepared to integrate C++26 reflections when they are available + +## Requirements + +A compiler with C++20 support. The library is tested with GCC 13 and later, Clang 17 and later, +Apple Clang, MSVC and clang-cl. It is a single header without dependencies beyond the standard library. + +Reflection works on aggregates: types without user-declared constructors, private members or base classes. +Members may be of any type, including types without a default constructor. An aggregate may have up to +150 members. + +With MSVC, member names are only available for types with external linkage, that is, not for types declared +in an anonymous namespace or inside a function. + +## Usage + +```cpp +#include + +struct Person +{ + std::string name; + std::string email; + int age; +}; + +auto person = Person { .name = "John Doe", .email = "john@doe.com", .age = 42 }; +``` + +Everything about the type itself is available at compile time: + +```cpp +static_assert(Reflection::CountMembers == 3); +static_assert(Reflection::MemberNameOf<0, Person> == "name"); +static_assert(Reflection::MemberNames[2] == "age"); +static_assert(std::same_as, int>); +static_assert(Reflection::TypeNameOf == "Person"); +static_assert(Reflection::NameOf<&Person::email> == "email"); +static_assert(Reflection::MemberIndexOf<&Person::email> == 1); +``` + +Members of an object are accessed by reference, and never copied: + +```cpp +Reflection::GetMemberAt<2>(person) = 43; + +// name and value of each member +Reflection::CallOnMembers(person, [](std::string_view name, auto const& value) { + std::println("{}: {}", name, value); +}); + +// index of each member as a template argument, e.g. to look up something else by it +Reflection::EnumerateMembers(person, [](auto& value) { + std::println("{}: {}", Reflection::MemberNameOf, value); +}); + +// only the members at index 0 and 2 +Reflection::EnumerateMembers>(person, [](auto& value) { /* ... */ }); +``` + +A type can be traversed without having an object of it: + +```cpp +Reflection::EnumerateMembers([]() { + std::println("{} is a {}", Reflection::MemberNameOf, Reflection::TypeNameOf); +}); + +// counting the members of type std::string +auto const stringCount = Reflection::FoldMembers(size_t { 0 }, [](size_t count) { + return count + (std::same_as ? 1 : 0); +}); +``` + +Folding over the members of an object: + +```cpp +auto const totalLength = Reflection::FoldMembers(person, size_t { 0 }, [](std::string_view name, auto const& /*value*/, size_t length) { + return length + name.size(); +}); +``` + +For debugging, objects can be printed and compared: + +```cpp +Reflection::Inspect(person); // name="John Doe" email="john@doe.com" age=43 + +auto other = person; +other.age = 50; +Reflection::CollectDifferences(person, other, [](std::string_view name, auto const& lhs, auto const& rhs) { + std::println("{}: {} != {}", name, lhs, rhs); // age: 43 != 50 +}); +``` diff --git a/include/reflection-cpp/reflection.hpp b/include/reflection-cpp/reflection.hpp index c34e1a5..2566cb0 100644 --- a/include/reflection-cpp/reflection.hpp +++ b/include/reflection-cpp/reflection.hpp @@ -33,6 +33,9 @@ namespace Reflection { +/// A string usable as a template argument, e.g. `template struct Tagged;` +/// +/// N is the size of the string literal it is created from, including the terminating null character. template struct StringLiteral { @@ -105,7 +108,9 @@ namespace detail } } // namespace detail -// Helper to get the value out +/// Concatenates strings at compile time into a single std::string_view. +/// +/// @tparam Strs The strings to join, each one a std::string_view with static storage duration template inline constexpr auto JoinStringLiterals = detail::join(); @@ -220,7 +225,9 @@ namespace detail #endif } // namespace detail -// Count the number of members in an aggregate type. +/// The number of members of an aggregate type. +/// +/// The members may be of types without a default constructor. template requires(std::is_aggregate_v>) constexpr inline auto CountMembers = detail::CountMembers>; @@ -1049,16 +1056,18 @@ constexpr decltype(auto) GetMemberAt(T&& t) return std::get(ToTuple(std::forward(t))); } -/// Represents the type of the member at index I of type Object +/// Represents the type of the member at index I of type Object, without reference or cv-qualifiers template using MemberTypeOf = std::remove_cvref_t(ToTuple(std::declval())))>; +/// Wraps a pointer, to pass the address of a member as a template argument. template struct WrappedPointer final { T* pointer; }; +/// Gets the address of the member at index N of an aggregate. template constexpr auto GetElementPtrAt(T&& t) noexcept { @@ -1068,8 +1077,35 @@ constexpr auto GetElementPtrAt(T&& t) noexcept namespace detail { +#if defined(_MSC_VER) && !defined(__clang__) + // An object of type T that only has to exist at compile time: only the addresses of its members are used. + // + // MSVC requires T to have external linkage for this, and does not accept the alternative below either, + // so member names of types in anonymous namespaces or at function scope are not available with it. template extern const T External; +#else + // An object that is declared but never defined, as above, is rejected for types without external linkage, + // as no other translation unit could define it. A union holding the object as a member that is never + // constructed is a real definition, and still does not require T to be constructible. + template + union UninitializedStorage { + constexpr UninitializedStorage() noexcept: + dummy {} + { + } + constexpr ~UninitializedStorage() {} + + char dummy; + T value; + }; + + template + inline constexpr UninitializedStorage ExternalStorage {}; + + template + inline constexpr T const& External = ExternalStorage.value; +#endif #if defined(__clang__) #pragma clang diagnostic push @@ -1122,11 +1158,16 @@ namespace detail }; } // namespace detail +/// The name of the member at index N of the aggregate type T, as a std::string_view. template inline constexpr auto MemberNameOf = []() constexpr { return detail::MemberNameOfImpl::stripped_literal; }(); +/// The name of the type T, including its namespace, as a std::string_view. +/// +/// The spelling of the name is the compiler's, and differs between compilers for some types, +/// such as templates and arrays. template constexpr auto TypeNameOf = [] { constexpr std::string_view name = detail::MangledName(); @@ -1185,6 +1226,7 @@ namespace detail } } // namespace detail +/// The names of all members of the aggregate type T, as a std::array of std::string_view. template inline constexpr auto MemberNames = [] { return detail::MemberNamesImpl(std::make_index_sequence> {}); @@ -1254,6 +1296,7 @@ namespace detail } // namespace detail +/// Calls `f()` for every I in the half-open range [B, E), with I as a template argument. template constexpr void template_for(F&& f) { @@ -1264,6 +1307,9 @@ constexpr void template_for(F&& f) }(std::make_integer_sequence {}); } +/// Calls `f()` for every element I of ElementMask, in order, with I as a template argument. +/// +/// @tparam ElementMask A std::integer_sequence listing the values to call f with template constexpr void template_for(F&& f) { @@ -1341,31 +1387,36 @@ template constexpr size_t MemberIndexOf = detail::MemberIndexHelperImpl

(std::make_index_sequence>> {}); -/// Calls a callable on members of an object specified with ElementMask sequence with the index of the member as the -/// first argument. and the member's default-constructed value as the second argument. +/// Calls a callable on the members of an object selected by ElementMask, with the index of the member as +/// template argument and a reference to the member as argument. +/// +/// @tparam ElementMask A std::integer_sequence listing the indices of the members to visit template constexpr void EnumerateMembers(Object& object, Callable&& callable) { template_for([&]() { callable.template operator()(GetMemberAt(object)); }); } -/// Calls a callable on members of an object specified with ElementMask sequence with the index and member's type as -/// template arguments. +/// Calls a callable for the members of a type selected by ElementMask, with the index and the type of the member +/// as template arguments. No object of the type is needed. +/// +/// @tparam ElementMask A std::integer_sequence listing the indices of the members to visit template constexpr void EnumerateMembers(Callable&& callable) { template_for([&]() { callable.template operator()>(); }); } -/// Calls a callable on each member of an object with the index of the member as the first argument. -/// and the member's default-constructed value as the second argument. +/// Calls a callable on each member of an object, with the index of the member as template argument +/// and a reference to the member as argument. template constexpr void EnumerateMembers(Object& object, Callable&& callable) { template_for<0, CountMembers>([&]() { callable.template operator()(GetMemberAt(object)); }); } -/// Calls a callable on each member of an object with the index and member's type as template arguments. +/// Calls a callable for each member of a type, with the index and the type of the member as template arguments. +/// No object of the type is needed. template constexpr void EnumerateMembers(Callable&& callable) { @@ -1378,6 +1429,7 @@ constexpr void EnumerateMembers(Callable&& callable) // clang-format on } +/// Calls a callable on each member of an object, with the name of the member and a reference to the member. template requires(CountMembers == 0) || std::is_invocable_v> void CallOnMembers(Object& object, Callable&& callable) @@ -1400,7 +1452,7 @@ 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 member's type 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 @@ -1555,6 +1607,7 @@ std::string Inspect(Object const& object) return str; } +/// Creates a human readable representation of a list of objects, one object per line. template std::string Inspect(std::vector const& objects) { diff --git a/test-reflection-cpp.cpp b/test-reflection-cpp.cpp index 4925221..ffa0e11 100644 --- a/test-reflection-cpp.cpp +++ b/test-reflection-cpp.cpp @@ -58,6 +58,46 @@ struct WithReq2 NoDefaultCtor b; }; +// MSVC cannot provide member names for types without external linkage. +#if !defined(_MSC_VER) || defined(__clang__) + +namespace +{ +struct InternalRecord +{ + int first; + std::string second; +}; +} // namespace + +TEST_CASE("internal linkage types", "[reflection]") +{ + static_assert(Reflection::CountMembers == 2); + static_assert(Reflection::MemberNameOf<0, InternalRecord> == "first"); + static_assert(Reflection::MemberNameOf<1, InternalRecord> == "second"); + static_assert(Reflection::NameOf<&InternalRecord::second> == "second"); + static_assert(Reflection::MemberIndexOf<&InternalRecord::second> == 1); + + CHECK(Reflection::Inspect(InternalRecord { .first = 1, .second = "two" }) == R"(first=1 second="two")"); +} + +TEST_CASE("function local types", "[reflection]") +{ + struct LocalRecord + { + int alpha; + std::string beta; + }; + + static_assert(Reflection::CountMembers == 2); + static_assert(Reflection::MemberNameOf<0, LocalRecord> == "alpha"); + static_assert(Reflection::MemberNameOf<1, LocalRecord> == "beta"); + + CHECK(Reflection::Inspect(LocalRecord { .alpha = 1, .beta = "two" }) == R"(alpha=1 beta="two")"); +} + +#endif + TEST_CASE("MemberIndex", "[reflection]") { static_assert(Reflection::MemberIndexOf<&Person::name> == 0);