174 lines
7.2 KiB
Text
174 lines
7.2 KiB
Text
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// Compile-time introspection of aggregates without P2996 (clang 23 has no
|
||
|
|
// <meta>). 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 <meta>, this partition is the only thing to replace.
|
||
|
|
export module Crafter.DB:Reflect;
|
||
|
|
import std;
|
||
|
|
|
||
|
|
namespace Crafter::DB::Reflect {
|
||
|
|
template<class T>
|
||
|
|
struct CountHelper {
|
||
|
|
template<class U>
|
||
|
|
static consteval auto Count(U& u) {
|
||
|
|
auto&& [...m] = u;
|
||
|
|
return std::integral_constant<std::size_t, sizeof...(m)>{};
|
||
|
|
}
|
||
|
|
static constexpr std::size_t value = decltype(Count(std::declval<T&>()))::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<class T>
|
||
|
|
[[clang::no_destroy]] inline const T external{};
|
||
|
|
|
||
|
|
template<auto P>
|
||
|
|
consteval std::string_view RawName() {
|
||
|
|
return std::source_location::current().function_name();
|
||
|
|
}
|
||
|
|
|
||
|
|
template<class T>
|
||
|
|
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<class T>
|
||
|
|
concept Aggregate = std::is_aggregate_v<T> && !std::is_array_v<T> && requires(T& t) { Reflect::CountHelper<T>::value; };
|
||
|
|
|
||
|
|
template<Aggregate T>
|
||
|
|
inline constexpr std::size_t memberCount = Reflect::CountHelper<T>::value;
|
||
|
|
|
||
|
|
// Reference to the I-th member of an aggregate lvalue.
|
||
|
|
template<std::size_t I, class T>
|
||
|
|
constexpr decltype(auto) Member(T&& t) noexcept {
|
||
|
|
auto&& [...m] = t;
|
||
|
|
return (m...[I]);
|
||
|
|
}
|
||
|
|
|
||
|
|
template<std::size_t I, class T>
|
||
|
|
using MemberType = std::remove_cvref_t<decltype(Member<I>(std::declval<T&>()))>;
|
||
|
|
|
||
|
|
// 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<std::size_t I, Aggregate T>
|
||
|
|
consteval std::string_view MemberName() {
|
||
|
|
constexpr std::string_view raw = Reflect::RawName<&Member<I>(Reflect::external<T>)>();
|
||
|
|
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<class T>
|
||
|
|
consteval std::string_view TypeName() {
|
||
|
|
constexpr std::string_view raw = Reflect::RawTypeName<T>();
|
||
|
|
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<class Ptr>
|
||
|
|
struct MemberPointerTraits;
|
||
|
|
|
||
|
|
template<class C, class M>
|
||
|
|
struct MemberPointerTraits<M C::*> {
|
||
|
|
using Class = C;
|
||
|
|
using Type = M;
|
||
|
|
};
|
||
|
|
|
||
|
|
template<auto Ptr>
|
||
|
|
using MemberClass = typename MemberPointerTraits<decltype(Ptr)>::Class;
|
||
|
|
|
||
|
|
template<auto Ptr>
|
||
|
|
using MemberTypeOf = typename MemberPointerTraits<decltype(Ptr)>::Type;
|
||
|
|
|
||
|
|
inline constexpr std::size_t noMember = static_cast<std::size_t>(-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<auto A, auto B>
|
||
|
|
consteval bool SameMemberPointer() {
|
||
|
|
if constexpr (std::is_same_v<std::remove_cv_t<decltype(A)>, std::remove_cv_t<decltype(B)>>) 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<Aggregate T, auto Ptr>
|
||
|
|
consteval std::size_t MemberIndex() {
|
||
|
|
T t{};
|
||
|
|
std::size_t index = noMember;
|
||
|
|
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||
|
|
((void)[&] {
|
||
|
|
if constexpr (std::is_same_v<MemberType<I, T>, std::remove_cvref_t<decltype(t.*Ptr)>>) {
|
||
|
|
if (std::addressof(Member<I>(t)) == std::addressof(t.*Ptr)) index = I;
|
||
|
|
}
|
||
|
|
}(), ...);
|
||
|
|
}(std::make_index_sequence<memberCount<T>>{});
|
||
|
|
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<Aggregate T, class M>
|
||
|
|
std::size_t MemberIndexOf(M T::* ptr) noexcept {
|
||
|
|
const T& instance = Reflect::external<T>;
|
||
|
|
std::size_t index = noMember;
|
||
|
|
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||
|
|
((void)[&] {
|
||
|
|
if constexpr (std::is_same_v<MemberType<I, T>, M>) {
|
||
|
|
if (std::addressof(Member<I>(instance)) == std::addressof(instance.*ptr)) index = I;
|
||
|
|
}
|
||
|
|
}(), ...);
|
||
|
|
}(std::make_index_sequence<memberCount<T>>{});
|
||
|
|
return index;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calls f.template operator()<I>(member) for every member, in order.
|
||
|
|
template<Aggregate T, class F>
|
||
|
|
constexpr void ForEachMember(T& t, F&& f) {
|
||
|
|
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
||
|
|
(f.template operator()<I>(Member<I>(t)), ...);
|
||
|
|
}(std::make_index_sequence<memberCount<T>>{});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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<Probe>);
|
||
|
|
static_assert(memberCount<Probe> == 4);
|
||
|
|
static_assert(MemberName<0, Probe>() == "alpha");
|
||
|
|
static_assert(MemberName<2, Probe>() == "gamma");
|
||
|
|
static_assert(MemberName<3, Probe>() == "delta");
|
||
|
|
static_assert(MemberIndex<Probe, &Probe::gamma>() == 2);
|
||
|
|
static_assert(MemberIndex<Probe, &Probe::delta>() == 3);
|
||
|
|
static_assert(std::is_same_v<MemberType<1, Probe>, double>);
|
||
|
|
static_assert(std::is_same_v<MemberTypeOf<&Probe::gamma>, std::string>);
|
||
|
|
static_assert(TypeName<Probe>().ends_with("Probe"));
|
||
|
|
}
|