Lightweight 0.20261105.1
Loading...
Searching...
No Matches
Description.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include <reflection-cpp/reflection.hpp>
6
7#include <concepts>
8#include <cstddef>
9#include <type_traits>
10#include <utility>
11
12namespace Lightweight
13{
14
15namespace detail
16{
17 template <typename MemberPointer>
18 struct MemberPointee;
19
20 // Pack indexing without std::tuple. The pack is instantiated once, as the bases of one class, and
21 // each lookup is one overload resolution, so the cost does not depend on how the standard library
22 // implements std::tuple, which can recurse once per element for every index.
23 template <std::size_t I, auto Value>
24 struct IndexedValue
25 {
26 static constexpr auto value = Value;
27 };
28
29 template <typename Sequence, auto... Values>
30 struct IndexedValues;
31
32 template <std::size_t... Is, auto... Values>
33 struct IndexedValues<std::index_sequence<Is...>, Values...>: IndexedValue<Is, Values>...
34 {
35 };
36
37 template <std::size_t I, auto Value>
38 IndexedValue<I, Value> SelectIndexedValue(IndexedValue<I, Value> const&);
39
40 template <std::size_t I, typename T>
41 struct IndexedType
42 {
43 using type = T;
44 };
45
46 template <typename Sequence, typename... Types>
47 struct IndexedTypes;
48
49 template <std::size_t... Is, typename... Types>
50 struct IndexedTypes<std::index_sequence<Is...>, Types...>: IndexedType<Is, Types>...
51 {
52 };
53
54 template <std::size_t I, typename T>
55 IndexedType<I, T> SelectIndexedType(IndexedType<I, T> const&);
56
57 /// The type at index @p I of the pack @p Types.
58 template <std::size_t I, typename... Types>
59 using NthType = decltype(SelectIndexedType<I>(
60 std::declval<IndexedTypes<std::make_index_sequence<sizeof...(Types)>, Types...>>()))::type;
61
62 /// The value at index @p I of the pack @p Values.
63 template <std::size_t I, auto... Values>
64 inline constexpr auto NthValue =
65 decltype(SelectIndexedValue<I>(IndexedValues<std::make_index_sequence<sizeof...(Values)>, Values...> {}))::value;
66
67 template <typename Class, typename Member>
68 struct MemberPointee<Member Class::*>
69 {
70 using type = Member;
71 };
72} // namespace detail
73
74/// @brief Compile-time list of a record's members, stored as pointers-to-member.
75///
76/// This is the access vehicle used by Description. It is an empty type — the members are
77/// carried as non-type template parameters — so it is far cheaper to parse and instantiate than a
78/// `std::tuple` of heterogeneous pointer values (no class storage, no CTAD). Member types are
79/// recovered by indexing the pointer-to-member pack (see `TypeAt`) and iteration uses element-wise
80/// pack expansion, both of which are essentially free at compile time.
81///
82/// @ingroup DataMapper
83template <auto... MemberPointers>
85{
86 /// Number of members in the list.
87 static constexpr std::size_t Count = sizeof...(MemberPointers);
88
89 /// The (unwrapped) type of the member at index @p I.
90 ///
91 /// Indexes the pointer-to-member pack via `detail::NthType` rather than the
92 /// `__type_pack_element` builtin: the latter is a Clang/GCC extension that MSVC does not
93 /// accept as a type (it parses the bare identifier as a function template — error C7568).
94 template <std::size_t I>
95 using TypeAt = detail::MemberPointee<detail::NthType<I, decltype(MemberPointers)...>>::type;
96
97 /// Invokes `callable<I>(record.member_I)` for every member, in order.
98 ///
99 /// The callable is forwarded once into a named reference and then invoked for each member, so a
100 /// `mutable` visitor (e.g. one carrying a running column offset) keeps its state across members.
101 template <typename Record, typename Callable>
102 static constexpr void EnumerateValues(Record& record, Callable&& callable)
103 {
104 auto&& visitor = std::forward<Callable>(callable);
105 // Expand only the index pack and reach each member through `MemberAt<I>`. Expanding the
106 // enclosing class-template NTTP pack (`MemberPointers...`) directly inside this nested
107 // lambda makes MSVC mis-bind the name (error C7746 "cannot appear in its own initializer");
108 // routing the access through the `MemberAt` helper — as `EnumerateMaskedValues` already
109 // does — sidesteps the defect with identical semantics and ordering.
110 [&]<std::size_t... I>(std::index_sequence<I...> /*indices*/) {
111 (visitor.template operator()<I>(MemberAt<I>(record)), ...);
112 }(std::make_index_sequence<Count> {});
113 }
114
115 /// Invokes `callable<I, MemberType>()` for every member, in order.
116 template <typename Callable>
117 static constexpr void EnumerateTypes(Callable&& callable)
118 {
119 auto&& visitor = std::forward<Callable>(callable);
120 [&]<std::size_t... I>(std::index_sequence<I...> /*indices*/) {
121 (visitor.template operator()<I, TypeAt<I>>(), ...);
122 }(std::make_index_sequence<Count> {});
123 }
124
125 /// Invokes `callable<I>(record.member_I)` for the members whose indices appear in @p ElementMask
126 /// (a `std::integer_sequence<std::size_t, ...>`), in mask order. Backs partial-column queries.
127 template <typename ElementMask, typename Record, typename Callable>
128 static constexpr void EnumerateMaskedValues(Record& record, Callable&& callable)
129 {
130 auto&& visitor = std::forward<Callable>(callable);
131 [&]<std::size_t... I>(std::integer_sequence<std::size_t, I...> /*mask*/) {
132 (visitor.template operator()<I>(MemberAt<I>(record)), ...);
133 }(ElementMask {});
134 }
135
136 /// Returns a reference to the member at index @p I of @p record.
137 template <std::size_t I, typename Record>
138 static constexpr decltype(auto) MemberAt(Record& record)
139 {
140 return (record.*(detail::NthValue<I, MemberPointers...>) );
141 }
142};
143
144/// @brief Customization point providing pre-computed reflection metadata for a record type.
145///
146/// The DataMapper reflects over records to discover their fields, field names, and types. By
147/// default this is done at compile time via the reflection-cpp library, which is accurate but
148/// expensive to instantiate for records with many members and dense relationship graphs (the
149/// cost is paid once per record type per translation unit and multiplies across the reachable
150/// relationship graph).
151///
152/// Tools such as `ddl2cpp` know a record's structure ahead of time and can emit an explicit
153/// specialization of this template. When a specialization exists the DataMapper reads the
154/// pre-baked metadata instead of evaluating reflection, which dramatically reduces compile time.
155/// Records without a specialization keep working unchanged via automatic reflection — the same
156/// "specialize-or-reflect" approach already used for the optional `static TableName` member.
157///
158/// A specialization must provide:
159/// - `static constexpr std::size_t FieldCount;` — number of members.
160/// - `using Members = RecordMemberList<&Record::a, &Record::b, ...>;` — the members, in
161/// declaration order.
162/// - `static constexpr std::array<std::string_view, FieldCount> FieldNames;` — the resolved SQL
163/// column name for each field (i.e. the value `FieldNameAt` would otherwise compute).
164///
165/// `Members` must list *every* non-static data member in declaration order, relation members
166/// (`HasMany`, `HasManyThrough`, `HasOneThrough`) included — it stands in for reflection, so omitting
167/// one hides it from `EnumerateRecordMembers` and relation auto-loading silently never runs.
168/// Consumers that want only the columns filter by the `RecordColumnMember` concept (see
169/// `RecordColumnCount`), never by index. A relation has no SQL column, so its `FieldNames` slot
170/// carries its C++ member name, which is what reflection reports there.
171///
172/// @ingroup DataMapper
173template <typename Record>
175
176/// @brief Satisfied when a Description specialization exists for the given record type.
177/// @ingroup DataMapper
178template <typename Record>
179concept HasDescription = requires {
180 { Description<std::remove_cvref_t<Record>>::FieldCount } -> std::convertible_to<std::size_t>;
181};
182
183namespace detail
184{
185 // Lazy dispatch for the member type: only the selected branch is instantiated, so the
186 // (expensive) reflection alias is never expanded when a descriptor is present.
187 template <std::size_t I, typename Record, bool HasDescriptor>
188 struct RecordMemberTypeOfDispatch
189 {
190 using type = Description<Record>::Members::template TypeAt<I>;
191 };
192
193 template <std::size_t I, typename Record>
194 struct RecordMemberTypeOfDispatch<I, Record, false>
195 {
196 using type = Reflection::MemberTypeOf<I, Record>;
197 };
198} // namespace detail
199
200/// @brief Number of members in a record — from the descriptor if present, else via reflection.
201/// @ingroup DataMapper
202template <typename Record>
203constexpr std::size_t RecordMemberCount = []() constexpr {
204 if constexpr (HasDescription<Record>)
205 return Description<std::remove_cvref_t<Record>>::FieldCount;
206 else
207 return Reflection::CountMembers<Record>;
208}();
209
210/// @brief Type of the member at index @p I — from the descriptor if present, else via reflection.
211/// @ingroup DataMapper
212template <std::size_t I, typename Record>
213using RecordMemberTypeOf = detail::RecordMemberTypeOfDispatch<I, std::remove_cvref_t<Record>, HasDescription<Record>>::type;
214
215/// @brief Returns a reference to the member at index @p I — from the descriptor if present, else via reflection.
216/// @ingroup DataMapper
217template <std::size_t I, typename Record>
218constexpr decltype(auto) GetRecordMemberAt(Record&& record)
219{
220 if constexpr (HasDescription<Record>)
221 return Description<std::remove_cvref_t<Record>>::Members::template MemberAt<I>(record);
222 else
223 return Reflection::GetMemberAt<I>(std::forward<Record>(record));
224}
225
226/// @brief Invokes @p callable as `callable<I>(member)` for each member of @p record.
227///
228/// Mirrors `Reflection::EnumerateMembers(object, callable)` but reads the descriptor when present,
229/// avoiding the aggregate-decomposition (`ToTuple`) instantiation.
230/// @ingroup DataMapper
231template <typename Record, typename Callable>
232constexpr void EnumerateRecordMembers(Record& record, Callable&& callable)
233{
234 if constexpr (HasDescription<Record>)
235 Description<std::remove_cvref_t<Record>>::Members::EnumerateValues(record, std::forward<Callable>(callable));
236 else
237 Reflection::EnumerateMembers(record, std::forward<Callable>(callable));
238}
239
240/// @brief Invokes @p callable as `callable<I>(member)` for each member selected by @p ElementMask.
241///
242/// Mirrors `Reflection::EnumerateMembers<ElementMask>(object, callable)` (partial-column queries)
243/// but reads the descriptor when present. @p ElementMask is a `std::integer_sequence<std::size_t, ...>`.
244/// @ingroup DataMapper
245template <typename ElementMask, typename Record, typename Callable>
246constexpr void EnumerateRecordMembers(Record& record, Callable&& callable)
247{
248 if constexpr (HasDescription<Record>)
249 Description<std::remove_cvref_t<Record>>::Members::template EnumerateMaskedValues<ElementMask>(
250 record, std::forward<Callable>(callable));
251 else
252 Reflection::EnumerateMembers<ElementMask>(record, std::forward<Callable>(callable));
253}
254
255/// @brief Invokes @p callable as `callable<I, MemberType>()` for each member of @p Record.
256///
257/// Mirrors `Reflection::EnumerateMembers<Object>(callable)` but reads the descriptor when present.
258/// @ingroup DataMapper
259template <typename Record, typename Callable>
260constexpr void EnumerateRecordMembers(Callable&& callable)
261{
262 if constexpr (HasDescription<Record>)
263 Description<std::remove_cvref_t<Record>>::Members::EnumerateTypes(std::forward<Callable>(callable));
264 else
265 Reflection::EnumerateMembers<Record>(std::forward<Callable>(callable));
266}
267
268/// @brief Folds over a record's members as `result = callable<I, MemberType>(result)`.
269///
270/// Mirrors `Reflection::FoldMembers<Object>(initialValue, callable)` but reads the descriptor when present.
271/// @ingroup DataMapper
272template <typename Record, typename Callable, typename ResultType>
273constexpr ResultType FoldRecordMembers(ResultType initialValue, Callable const& callable)
274{
275 ResultType result = initialValue;
276 EnumerateRecordMembers<Record>(
277 [&]<std::size_t I, typename MemberType>() { result = callable.template operator()<I, MemberType>(result); });
278 return result;
279}
280
281} // namespace Lightweight
Satisfied when a Description specialization exists for the given record type.
constexpr ResultType FoldRecordMembers(ResultType initialValue, Callable const &callable)
Folds over a record's members as result = callable<I, MemberType>(result).
constexpr decltype(auto) GetRecordMemberAt(Record &&record)
Returns a reference to the member at index I — from the descriptor if present, else via reflection.
constexpr std::size_t RecordMemberCount
Number of members in a record — from the descriptor if present, else via reflection.
constexpr void EnumerateRecordMembers(Record &record, Callable &&callable)
Invokes callable as callable<I>(member) for each member of record.
detail::RecordMemberTypeOfDispatch< I, std::remove_cvref_t< Record >, HasDescription< Record > >::type RecordMemberTypeOf
Type of the member at index I — from the descriptor if present, else via reflection.
Customization point providing pre-computed reflection metadata for a record type.
Compile-time list of a record's members, stored as pointers-to-member.
static constexpr std::size_t Count
Number of members in the list.
static constexpr void EnumerateMaskedValues(Record &record, Callable &&callable)
static constexpr void EnumerateTypes(Callable &&callable)
Invokes callable<I, MemberType>() for every member, in order.
detail::MemberPointee< detail::NthType< I, decltype(MemberPointers)... > >::type TypeAt
static constexpr decltype(auto) MemberAt(Record &record)
Returns a reference to the member at index I of record.
static constexpr void EnumerateValues(Record &record, Callable &&callable)