Crafter.DB/interfaces/Crafter.DB-Reflect.cppm

174 lines
7.2 KiB
Text
Raw Normal View History

// 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"));
}