From 9e304536b5ee978c4fca5e3364239f39cc12ee2a Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 1 Oct 2026 19:21:24 +0200 Subject: [PATCH 1/6] ci: Remove the outdated note about a linker error with MSVC The MSVC jobs build, link and pass the tests. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ar6Ahymw9LhLsnM324z5cV Signed-off-by: Yaraslau Tamashevich --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) 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 From f8acccfb88df4b8e76737066a10575c0b192f281 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 1 Oct 2026 19:22:30 +0200 Subject: [PATCH 2/6] Support types in anonymous namespaces and at function scope Member names are read from the address of a member of an object that only needs to exist at compile time. That object was a variable declared `extern` and never defined, which compilers reject for types without external linkage, since no other translation unit could define it. Use a union holding the object as a member that is never constructed instead. It is a real definition, so it works for every type, and still does not require the type to be constructible. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ar6Ahymw9LhLsnM324z5cV Signed-off-by: Yaraslau Tamashevich --- include/reflection-cpp/reflection.hpp | 18 +++++++++++++- test-reflection-cpp.cpp | 35 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/include/reflection-cpp/reflection.hpp b/include/reflection-cpp/reflection.hpp index c34e1a5..b55ca73 100644 --- a/include/reflection-cpp/reflection.hpp +++ b/include/reflection-cpp/reflection.hpp @@ -1068,8 +1068,24 @@ constexpr auto GetElementPtrAt(T&& t) noexcept namespace detail { + // Storage for an object of type T that is never constructed: only the addresses of its members are used. template - extern const T External; + 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; #if defined(__clang__) #pragma clang diagnostic push diff --git a/test-reflection-cpp.cpp b/test-reflection-cpp.cpp index 4925221..45d3964 100644 --- a/test-reflection-cpp.cpp +++ b/test-reflection-cpp.cpp @@ -58,6 +58,41 @@ struct WithReq2 NoDefaultCtor b; }; +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")"); +} + TEST_CASE("MemberIndex", "[reflection]") { static_assert(Reflection::MemberIndexOf<&Person::name> == 0); From dc1c5c59181d7ffe8357491b2302e311ecf10ec5 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 1 Oct 2026 19:31:58 +0200 Subject: [PATCH 3/6] Keep the declared-only stand-in object for MSVC MSVC does not accept the address of a member of a union member that was never constructed as a constant expression, so it keeps using the object that is declared but never defined. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ar6Ahymw9LhLsnM324z5cV Signed-off-by: Yaraslau Tamashevich --- include/reflection-cpp/reflection.hpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/include/reflection-cpp/reflection.hpp b/include/reflection-cpp/reflection.hpp index b55ca73..4c5c5ff 100644 --- a/include/reflection-cpp/reflection.hpp +++ b/include/reflection-cpp/reflection.hpp @@ -1068,7 +1068,14 @@ constexpr auto GetElementPtrAt(T&& t) noexcept namespace detail { - // Storage for an object of type T that is never constructed: only the addresses of its members are used. +#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. + template + extern const T External; +#else + // An object that is declared but never defined, as above, is rejected by GCC and Clang for types + // without external linkage, as no other translation unit could define it. They accept a union holding + // the object as a member that is never constructed, which MSVC does not treat as a constant in turn. template union UninitializedStorage { constexpr UninitializedStorage() noexcept: @@ -1086,6 +1093,7 @@ namespace detail template inline constexpr T const& External = ExternalStorage.value; +#endif #if defined(__clang__) #pragma clang diagnostic push From 7614ba664eeecd56184023887a362fea2d414784 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 1 Oct 2026 19:23:47 +0200 Subject: [PATCH 4/6] Document the public API Add a usage section to the README, and a description to every public declaration that had none. The descriptions of EnumerateMembers claimed the callable receives a default-constructed value, while it receives a reference to the member. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ar6Ahymw9LhLsnM324z5cV Signed-off-by: Yaraslau Tamashevich --- README.md | 88 +++++++++++++++++++++++++++ include/reflection-cpp/reflection.hpp | 48 +++++++++++---- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b18a68f..0a677e3 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,91 @@ 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. + +## 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 4c5c5ff..842a61c 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 { @@ -1146,11 +1155,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(); @@ -1209,6 +1223,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> {}); @@ -1278,6 +1293,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) { @@ -1288,6 +1304,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) { @@ -1365,31 +1384,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) { @@ -1402,6 +1426,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) @@ -1424,7 +1449,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 @@ -1579,6 +1604,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) { From ba71a297a6c3874bd7c1aaf5ff1e07f2bfa7523a Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 1 Oct 2026 19:38:10 +0200 Subject: [PATCH 5/6] Document that MSVC needs external linkage for member names MSVC rejects the declared-only stand-in object for types without external linkage (C7631), and does not accept the union used by the other compilers either. Member names of types in anonymous namespaces or at function scope therefore remain unavailable with MSVC. Say so in the README and next to the code, and skip the tests for it there. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ar6Ahymw9LhLsnM324z5cV Signed-off-by: Yaraslau Tamashevich --- README.md | 3 +++ include/reflection-cpp/reflection.hpp | 9 ++++++--- test-reflection-cpp.cpp | 5 +++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0a677e3..b3ae781 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Reflection works on aggregates: types without user-declared constructors, privat 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 diff --git a/include/reflection-cpp/reflection.hpp b/include/reflection-cpp/reflection.hpp index 842a61c..2566cb0 100644 --- a/include/reflection-cpp/reflection.hpp +++ b/include/reflection-cpp/reflection.hpp @@ -1079,12 +1079,15 @@ 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 by GCC and Clang for types - // without external linkage, as no other translation unit could define it. They accept a union holding - // the object as a member that is never constructed, which MSVC does not treat as a constant in turn. + // 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: diff --git a/test-reflection-cpp.cpp b/test-reflection-cpp.cpp index 45d3964..ffa0e11 100644 --- a/test-reflection-cpp.cpp +++ b/test-reflection-cpp.cpp @@ -58,6 +58,9 @@ struct WithReq2 NoDefaultCtor b; }; +// MSVC cannot provide member names for types without external linkage. +#if !defined(_MSC_VER) || defined(__clang__) + namespace { struct InternalRecord @@ -93,6 +96,8 @@ TEST_CASE("function local types", "[reflection]") 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); From 5c75b9856a85bdcbbe115ed12781b61180288679 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 1 Oct 2026 19:23:47 +0200 Subject: [PATCH 6/6] Bump version to 0.5.0 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Ar6Ahymw9LhLsnM324z5cV Signed-off-by: Yaraslau Tamashevich --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)