Open willwray opened 6 years ago
@willwray, I have some questions about your approach, may we discuss their (by email or here for example)? May you show me your c++17 implementation of this scheme?
Please, provide a PR
Crikey!
This should be a bit easier now...
The 'Array vs Scalar init' issue turned out to be a compiler bug; the standard says that scalars should be brace-initializable too. I submitted the gcc bug and patch (released in gcc 9), discussed the clang bug (and Mr Smith promptly fixed it in clang 8).
This means that braced-init really is 'uniform' which simplifies things. The tentative initializers can now consistently use braced-int. (I don't know the situation in MSVC.)
The most straightforward approach will be via structured binding, assuming that members can be correctly counted now using braced init.
I.e. hack pfr/detail/fields_count.hpp
to count array members
without breaking its many special-case workarounds
then dispatch to pfr/detail/core17_generated.hpp
.
The 'init-list sniffing' approach is complicated by C array bounds deduction for large or possibly multidimensional arrays.
Reflection of member array types is possible by refining magic_get methods.
Recursive scheme
Here's a scheme for finding an aggregate's member types one at a time:
Given an aggregate-initializable struct
S
:T...
ofS
are known (none to start) =>S
can be aggregate initialized asS { T_init... }
withT_init
braced or unbraced initialization as needed byT
(scalars need unbraced init, arrays need braced init => awkward).S { T_init..., ubiq_init }
whereubiq
is the usual convert-to-anything type. If this fails thenT...
is the complete list of members: Done.L
(either by 'Great Type Loophole' or by typeid memo). If the member is array type thenL
is its base type (on gcc ubiq actually sees the full array type - a tempting anomaly)S { T_init..., {L_init} }
with increasing depth of nested braces => deduce the rankS { T_init..., {{Li...}} }
S { T_init..., {{Li}...} }
with increasing numbers Li... => the maxes are the array boundsT...
Array vs Scalar init
Arrays and scalar types don't mix well in braced-init lists, requiring non-uniform initialization; to support both requires expanding both braced and unbraced initialization syntax. Again, given aggregate-initializable struct
S
with known initial member type sequenceT...
S{ T{}... }
fails forT = L[N]
array typearray must be initialized with a brace-enclosed initializer
.S{ {T{}}... }
fails for scalarerror: braces around scalar initializer
A 'Catch 22' situation in generating a suitable init-list:
Workarounds for non-uniform initialization syntax
For the initial member type initializations:
T...
into runs requiring braced or unbraced initialization:T...
-> Ts<U0....>, Ts<B0....> ... Ts<UN-1...>, Ts<BN-1...>A different workaround would be to flatten all arrays and do all unbraced inits. For large arrays this explodes compile time and memory usage. Or, for a mixed workaround, fallback to flattening arrays only when necessary. If more than N pairs of runs are needed, selectively flatten smaller arrays in order to remove braced runs until there are only N pairs of runs.
Proof of Concept code
I have code for this scheme, done in C++17 plus
-fconcepts
for ease (so currently limited to gcc, but not using gcc's direct extraction of the array type). It is prototype / experimental code that has not been much exercised yet. It is around 500 LOC including comments. It uses the Loophole ubiq. It expands up to 4 array dimensions. It expands up to 4 pairs of unbraced,braced runs (the array-flattening fallback has not been implemented so 4 is a hard limit). In principle, it should be possible to backport to C++14. I'm not motivated to backport or test with Clang / MSVC until they have concepts. Compile time for large arrays gets noticeable, likely the binary search for bounds.Contribute to pfr / magic_get?
I'm happy to provide the code in current form, to see if it is suitable for inclusion. There seem to be more cons than pros though...
Cons: Arrays are awkward types so still need special-casing in generic client code. Don't want to encourage C-array usage - std::array is better if it can be used. Consumes more compile resource than current 'precise' and 'flat' methods. It comes burdened with implementation-defined limits. The code is involved so there's a maintenance cost. The backport is not trivial.
Pros: Its the only way I know to reflect array members. Because it's there.
Note on structured bindings and 'structured reflection'
Structured bindings get tantalisingly close to complete 'precise' struct reflection; if you know the member count in a struct then you can bind to and get all types. If you don't know then you have to count, but you can't directly count because you can't SFINAE on a structured binding failing, so you have to count indirectly. For an aggregate, this means counting via the init-list. We're back to init, innit. Egg-bound by the chickens.
I floated this template-matching syntax for 'structured reflection';
If only it were this simple.