📦 csl::ag
Tuple-like interface for aggregate types.
|
The goal of csl::ag
is to offer convenient ways to manipulate aggregate types.
The following example demonstrates some of the features which are available in csl::ag
.
C++ code ( Try me on compiler-explorer ) | Console output |
---|---|
struct S { char c; int i; };
static_assert(
csl::ag::concepts::aggregate<S> and
csl::ag::size_v<S> == 2
);
static_assert(std::same_as<char, csl::ag::element_t<0, S>>);
static_assert(std::same_as<int, csl::ag::element_t<1, S>>);
S value{ 'A', 41 }; ++std::get<1>(value);
using namespace csl::ag::io;
std::cout << "value: " << value << '\n';
// (wip) compatibility with `fmt` and `std::print` will be available soon
|
value: S& : {
[0] char : A
[1] int : 42
}
|
By default, the C++ standard allow structured-binding for aggregate types.
C++ code ( Try me on compiler-explorer ) |
---|
struct type{ int i; char c; };
auto value = type{ 42, 'A' }; // NOLINT
[[maybe_unused]] auto && [ v0, v1 ] = value;
assert(v0 == 42); // pass
assert(v1 == 'A'); // pass
|
However, there is no - simple - way to access the following informations for a given aggregate type or value :
This library provides a way to obtain such information, and internally use it to provide convenient high-level conversions and printing functions.
This library is divided in five distinct parts :
std::ostream & operator<<
overloads or fmt
)The key idea of this library is to ease iterations over aggregates's member-variables,
which is especially convenient when dealing with reflection and serialization.
csl::ag::size<T>
gives the fields count in a given aggregate type type to_tuple
or to_tuple_view
conversion)csl::ag::get<size_t N>(aggregate auto value)
allows per-field access, in a similar way to std::get<N> for std::tuple<Ts...>This library is single-header, header-only. Users may use it in various ways, however CMake is the promoted one for both download and configuration.
⚠️ Proceeding the ways enumerated above is fast & simple. However this prevent users from using certain configuration mechanismes. See the configuration section for more information.
Then use the csl::ag
target.
Note : to disable tests, set the cmake cache variable
CSL_BUILD_ALL_TESTS
to false.
This project can be configured using the following cmake cache entries, grouped by categories:
⚠️ By default, bitfields support is disabled.
Using features for this library with any aggregate type using custom layout will results in ☣️ undefined behavior.
Most likely, a compile-time error will be emitted. However, such behavior is not guaranteed.
If you plan to use features of this library with aggregate types containing bitfields, you must first enable such support either using one of the two following ways :
CMake
, edit the cache to set the CSL_AG__ENABLE_BITFIELDS_SUPPORT
option to on
. CSL_AG__ENABLE_BITFIELDS_SUPPORT
. ❔ Question : Why such option exists ?
The (compile-time) algorithm internally used by the library to count fields for aggregate types possibly containing bitfields is much slower than the default one.
One might want to challenge his/her project's design in order to avoid such high performance cost.
This library relies on a CMake cache variable CSL_AG__MAX_FIELDS_SUPPORTED_COUNT
to generate code in order to properly handle aggregate types with fields up to this value.
By default, CSL_AG__MAX_FIELDS_SUPPORTED_COUNT
is set to 128
, meaning the library supports aggregate types with up to 128 fields.
To extend such support, edit your CMake cache to set CSL_AG__MAX_FIELDS_SUPPORTED_COUNT
to a greater integral value.
❔ Question : What if I don't use CMake ?
Then the library will always use the default value.
❔ Question : Why such configuration/limitation ?
Despite interesting proposals that aim to enhance & offer new code generation mecanisms as part of the C++ language, such features are not available yet.
The choice here to use CMake in order to generate C++ code upstream is a reasonable trade-off to guarantee easier debugging and avoid dark-magic tricks (such as relying on PP macros, etc.).
👉 If you are willing to propose a better design, you can submit a PR here.
⚠️ This section is experimentale, and SHOULD NOT be used in production.
Breaking changes are very likely, as the API is instable for now.
All options in this section are opt-ins *(OFF
by default)*
CSL_AG__ENABLE_FORMAT_SUPPORT
: add std::formatter<csl::ag::aggregate T>
CSL_AG__ENABLE_FMTLIB_SUPPORT
: makes csl::ag
depends on the fmt
library, and add fmt::formatter<csl::ag::aggregate T>
.If fmtlib's
cmake
targetfmt::fmt-header-only
is not available when buildingcsl::ag
withCSL_AG__ENABLE_FMTLIB_SUPPORT
set toON
, then such a dependency will be injected usingcmake FetchContent
.
CSL_AG__ENABLE_IOSTREAM_SUPPORT
: add csl::ag::io::operator<<(const csl::io::indented_ostream os, csl::ag::concepts::structured_bindable auto && value)
About compact vs. pretty presentations:
All components that are part of the public interface are defined in the namespace csl::ag
,
except for nested-namespaces named details
.
In other words, the library provides no guarantee to any direct use of namespaces named with a pattern like csl::ag::*::details::*
.
All concepts that are part of the public interface are defined in the namespace csl::ag::concepts
.
Requirements that given T
type must meet to be considered as an unqualified (e.g, not cvref-qualified) aggregate type by this library components.
std::is_aggregate_v<T>
not std::is_empty_v<T>
not std::is_union_v<T>
not std::is_polymorphic_v<T>
not std::is_reference_v<T>
T
must be a possibly cvref-qualified aggregate, meeting the unqualified_aggregate<std::remove_cvref_t<T>>
requirement.
Note that such requirement is widely used in this library.
T
must be a valid aggregate type, constructible using brace-initialization using values of types args_ts...
.
T
must be a valid aggregate type, constructible using N
values (which types does not matter here).
This does not mean that T
has N
fields : it can be more.
T
must meet the tuplelike interface, with valid implementation of :
T
must either match tuplelike<T>
or aggregate<T>
requirements.
See the structured_binding documentation for more details.
Integral constant type which value represents the count of fields for a given aggregate type.
Just like std::tuple_size
/std::tuple_size_v
, the value can be accessed using a convenience alias :
Type-identity of a field's type of a given aggregate type.
Just like std::tuple_element/std::tuple_element_t
, the type can be accessed using a convenience alias :
In a similar way to csl::ag::element<std::size_t, T>
, csl::ag::view_element<std::size_t,T>
is a type-identity for a field's type of a given aggregate view type.
For more details about aggregate's view, see the to-tuple non-owning conversion (view) section.
The type
nested-type can be accessed using a convenience alias :
This library provides two ways to convert an aggregate's value to std::tuple, distinguishing between proprietary and non-proprietary tuples of values.
Owning is a plain translation of an aggregate type as a tuple.
Each std::tuple_element_t
of the resulting type will be strictly equivalent to csl::ag::element_t
of the source one.
The value of each field is pass by-value (understand: copy).
See the Owning conversion section hereunder.
Non-owning (undestand: view, or lightweight accessor) conversion offers a cheap way to convert an aggregate into a tuple of references;
offering a convenient way to then use already-existing features - or even libraries - that operates on std::tuple values.
csl::ag::element_t
is strictly equivalent to std::tuple_element_t
.See the Non-owning conversion (view) section hereunder.
The following factory (as a function) creates a std::tuple
of csl::ag::elements
, never altered.
The value of each member-variable of the aggregate's value are forwarded, in order to preserve cvref-semantic.
Meaning that using a const-lvalue-reference
of a given type S
's value will result in a copy of each of its field that are not ref-qualified,
while using a rvalue-reference
will results in a perfect-forwarding that member-variable.
C++ code ( Try me on compiler-explorer ) | Console output |
---|---|
struct A{ int i; float f; };
constexpr auto value = A{ .i = 42, .f = 0.13f };
constexpr auto value_as_tuple = csl::ag::to_tuple(std::move(value));
[&]<std::size_t ... indexes>(std::index_sequence<indexes...>){
static_assert((std::same_as<
csl::ag::element_t<indexes, A>, // { 0: int, 1:float }
std::tuple_element_t<indexes, std::remove_cvref_t<decltype(value_as_tuple)>>
> and ...));
((std::cout << std::get<indexes>(value_as_tuple) << ' '), ...);
}(std::make_index_sequence<csl::ag::size_v<A>>{});
|
42 0.13
|
The main advantage here is to use such function in a constexpr
contexts.
A precondition while doing so is that each aggregates field's value must be usable in a constexpr context though (e.g not ref-qualified).
Additionaly, std::tuple_element_t can be use to obtains the conversion result's element types.
Example 1 : aggregate type with not-cvref-qualified fields
C++ code ( Try me on compiler-explorer ) | Console output |
---|---|
struct A{ int i; float f; };
constexpr auto value = csl::ag::to_tuple(A{ .i = 42, .f = 0.13f });
[&value]<std::size_t ... indexes>(std::index_sequence<indexes...>){
((std::cout << std::get<indexes>(value) << ' '), ...);
}(std::make_index_sequence<csl::ag::size_v<A>>{});
static_assert(std::same_as<
int,
std::tuple_element_t<0, std::remove_cvref_t<decltype(value)>>
>); // \-> same as csl::ag::to_tuple_t<A>
static_assert(std::same_as<
float,
std::tuple_element_t<1, std::remove_cvref_t<decltype(value)>>
>);
|
42 0.13
|
Example 2 : aggregate type with ref-qualified fields
C++ code ( Try me on compiler-explorer ) | Console output |
---|---|
struct A{ int & i; float && f; };
int i = 42; float f = .13f;
/* not constexpr */ auto value = csl::ag::to_tuple(A{ .i = i, .f = std::move(f) });
[&value]<std::size_t ... indexes>(std::index_sequence<indexes...>){
((std::cout << std::get<indexes>(value) << ' '), ...);
}(std::make_index_sequence<csl::ag::size_v<A>>{});
static_assert(std::same_as<
int&,
std::tuple_element_t<0, std::remove_cvref_t<decltype(value)>>
>);
static_assert(std::same_as<
float&&,
std::tuple_element_t<1, std::remove_cvref_t<decltype(value)>>
>);
|
42 0.13
|
This factory (as a function) creates non-owning lightweight accessors (views),
returning a non-owning tuple (std::tuple
of references), for which each element represents an accessor to an aggregate value's field.
Note that in order to preserve cvref semantic
, the possibly-used cvref qualifiers
of the aggregate's value are propagated to qualify each of non-ref-qualified elements of the result tuple-type.
Ref-qualified fields type remain unchanged.
The conversion's result type can be access using the tuple_view(_t)<T>
type-trait.
Additionally, csl::ag::view_element(_t)<N,T>
can be used to obtains a field's type information, by index.
Simple example :
Slightly more advanced example :
Note that constexpr
-ness is preserved :
There are two way to pretty-print aggregate types :
std::ostream& operator<<(std::ostream&, T&&)
overloadfmt
or std::format
librarySimple example :
Advanced example :
Output :
As is, it is quite easy to handle aggregates and tuple in an homogeneous way, despite limitation listed in the next section below.
As-is, this implementation internally relies on structured-binding, which design choice expose two main limitations :
std
namespace definitions for get
/tuple_element
/tuple_size(_v)
won't work).Everything has its own dirty secrets, and this library is no exception.
Internally, and for each given aggregate type, it recursively check if a value of the later is constructible from an aggregate-initialization using N
implicitly-castable-to-anything parameters values.
The initial
N
value issizeof(T)
.
If the result is a failure, then another attempt using N-1
is done, up to 1 (included).
See csl::ag::concepts::aggregate_with_n_fields<T, size>