// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Compile-time introspection of aggregates without P2996 (clang 23 has no // ). Three ingredients, all verified on the toolchain this library // targets: structured-binding packs give member count and per-member access, // std::source_location::function_name() leaks the member name when a pointer // to a member of an extern object is used as a template argument, and address // comparison on a constexpr instance maps a pointer-to-member to its index. // When a clang ships , this partition is the only thing to replace. export module Crafter.DB:Reflect; import std; namespace Crafter::DB::Reflect { template struct CountHelper { template static consteval auto Count(U& u) { auto&& [...m] = u; return std::integral_constant{}; } static constexpr std::size_t value = decltype(Count(std::declval()))::value; }; // A constant-initialised instance whose member addresses are turned into // text by function_name(). Only its address is ever used, in a constant // expression, so nothing reads or writes it at run time; no_destroy keeps // the compiler from registering a destructor for it, which lets the linker // drop the object entirely. (An undefined `extern` declaration would be // cheaper still, but clang warns about it at every instantiation.) template [[clang::no_destroy]] inline const T external{}; template consteval std::string_view RawName() { return std::source_location::current().function_name(); } template consteval std::string_view RawTypeName() { return std::source_location::current().function_name(); } } export namespace Crafter::DB { // True for class types whose non-static data members can be bound by a // structured binding: public members, no bases carrying members, no // user-declared constructors. Tables and nested column groups must be this. template concept Aggregate = std::is_aggregate_v && !std::is_array_v && requires(T& t) { Reflect::CountHelper::value; }; template inline constexpr std::size_t memberCount = Reflect::CountHelper::value; // Reference to the I-th member of an aggregate lvalue. template constexpr decltype(auto) Member(T&& t) noexcept { auto&& [...m] = t; return (m...[I]); } template using MemberType = std::remove_cvref_t(std::declval()))>; // The declared name of the I-th member, e.g. "iban". Parsed out of // "std::string_view Crafter::DB::Reflect::RawName() [P = &external.iban]". template consteval std::string_view MemberName() { constexpr std::string_view raw = Reflect::RawName<&Member(Reflect::external)>(); const std::size_t end = raw.rfind(']'); const std::size_t dot = raw.rfind('.', end); return raw.substr(dot + 1, end - dot - 1); } // The type's qualified name as the compiler spells it ("Nostro::Transaction"). // Used in diagnostics only, so the exact spelling is not load-bearing. template consteval std::string_view TypeName() { constexpr std::string_view raw = Reflect::RawTypeName(); const std::size_t end = raw.rfind(']'); const std::size_t eq = raw.rfind("T = ", end); return raw.substr(eq + 4, end - eq - 4); } template struct MemberPointerTraits; template struct MemberPointerTraits { using Class = C; using Type = M; }; template using MemberClass = typename MemberPointerTraits::Class; template using MemberTypeOf = typename MemberPointerTraits::Type; inline constexpr std::size_t noMember = static_cast(-1); // Equality of two pointers-to-member that may point into different member // types (comparing those directly is ill-formed): false unless the types // match and the pointers are equal. template consteval bool SameMemberPointer() { if constexpr (std::is_same_v, std::remove_cv_t>) return A == B; else return false; } // Index of the member a pointer-to-member designates, or noMember. Works // by comparing the address of each bound member with the address the // pointer selects on the same constexpr instance. template consteval std::size_t MemberIndex() { T t{}; std::size_t index = noMember; [&](std::index_sequence) { ((void)[&] { if constexpr (std::is_same_v, std::remove_cvref_t>) { if (std::addressof(Member(t)) == std::addressof(t.*Ptr)) index = I; } }(), ...); }(std::make_index_sequence>{}); return index; } // Runtime counterpart of MemberIndex for a pointer that is not a template // argument. Every call site passes a literal `&T::member`, and the probe // object is constant-initialised (no guard to check), so after inlining the // comparison chain folds to a constant. template std::size_t MemberIndexOf(M T::* ptr) noexcept { const T& instance = Reflect::external; std::size_t index = noMember; [&](std::index_sequence) { ((void)[&] { if constexpr (std::is_same_v, M>) { if (std::addressof(Member(instance)) == std::addressof(instance.*ptr)) index = I; } }(), ...); }(std::make_index_sequence>{}); return index; } // Calls f.template operator()(member) for every member, in order. template constexpr void ForEachMember(T& t, F&& f) { [&](std::index_sequence) { (f.template operator()(Member(t)), ...); }(std::make_index_sequence>{}); } } // Self-tests: a compiler whose function_name() text changes shape fails here, // at the library's build, instead of producing garbage column names. namespace Crafter::DB::Reflect { struct Probe { std::int32_t alpha; double beta; std::string gamma; std::chrono::system_clock::time_point delta; }; static_assert(Aggregate); static_assert(memberCount == 4); static_assert(MemberName<0, Probe>() == "alpha"); static_assert(MemberName<2, Probe>() == "gamma"); static_assert(MemberName<3, Probe>() == "delta"); static_assert(MemberIndex() == 2); static_assert(MemberIndex() == 3); static_assert(std::is_same_v, double>); static_assert(std::is_same_v, std::string>); static_assert(TypeName().ends_with("Probe")); }