Poor man's define_aggregate
TLDR: [Try it on Compiler Explorer](https://godbolt.org/z/sfczoP7hr).
----
While waiting for Clang to support `define_aggregate`, I got curious about whether it's possible to do something similar in C++23. Turns out it *kinda* is.
### Rules:
- Only C++23 features;
- No external programs;
- No macros;
- Generated code should be similar to just using a `struct`.
----
We start with some helper types:
#include <algorithm>
#include <array>
#include <concepts>
#include <functional>
#include <print>
#include <ranges>
#include <string_view>
#include <tuple>
#include <type_traits>
namespace detail
{
// See `type`
template <typename T>
struct FieldType
{
using Type = T;
};
// Helper for using a string as a template parameter
template <std::size_t size>
struct ConstexprStringHelper
{
std::array<char, size - 1> array;
constexpr ConstexprStringHelper(const char (&c_array)[size])
{
std::copy_n(c_array, size - 1, std::begin(array));
}
};
// See `operator""_field`
template <auto name>
struct FieldByName
{
};
We calculate the layout of our fake aggregate (sizes, alignment, offsets, ...) at compile time, like so:
// Layout information
template <auto... fields>
struct MetaAggregateInfo
{
static consteval auto calc_align(std::size_t offset, std::size_t align)
{
return (offset + align - 1) & ~(align - 1);
}
static constexpr std::array names{ std::string_view(fields.name)... };
static constexpr std::array sizes{ sizeof(typename decltype(fields)::Type)... };
static constexpr std::array aligns{ alignof(typename decltype(fields)::Type)... };
static constexpr auto max_align = std::ranges::max(aligns);
static constexpr auto offsets = [] {
std::remove_const_t<decltype(sizes)> offsets;
std::size_t next_offset = 0;
for (auto [size, align, offset] : std::views::zip(sizes, aligns, offsets))
{
offset = calc_align(next_offset, align);
next_offset = offset + size;
}
return offsets;
}();
static constexpr auto total_size = calc_align(offsets.back() + sizes.back(), max_align);
};
I found it simpler to just use a partial specialization for the case where the aggregate has no members:
template <>
struct MetaAggregateInfo<>
{
static constexpr std::array<std::string_view, 0> names{};
static constexpr std::array<std::size_t, 0> sizes{};
static constexpr std::array<std::size_t, 0> aligns{};
static constexpr auto max_align = 1uz;
static constexpr std::array<std::size_t, 0> offsets{};
static constexpr auto total_size = 1uz;
};
}
A few more helpers:
// Use to declare the type of a field. See example below.
template <typename T>
constexpr detail::FieldType<T> type;
// Type and name of a field
template <typename TheType, std::size_t size>
struct Field
{
using Type = TheType;
detail::FieldType<TheType> type;
std::array<char, size> name;
};
// Use to declare the name of a field
template <detail::ConstexprStringHelper helper>
consteval auto operator""_name()
{
return helper.array;
}
// Use with operator[] to access a field by name
template <detail::ConstexprStringHelper helper>
consteval auto operator""_field() -> detail::FieldByName<helper.array>
{
return {};
}
And now the meat of the code:
template <auto... fields>
class MetaAggregate
{
public:
static constexpr detail::MetaAggregateInfo<fields...> info{};
We define our constructors, copy/move operators and destructor. We use the offsets to get a pointer on which we can do a placement `new`. Other than that, this part is not very interesting.
MetaAggregate()
requires(std::default_initializable<typename decltype(fields)::Type> && ...)
{
std::apply(
[&](auto... offset) { (new (storage.data() + offset) decltype(fields)::Type(), ...); },
info.offsets
);
}
MetaAggregate(const MetaAggregate& other)
requires(std::copy_constructible<typename decltype(fields)::Type> && ...)
: MetaAggregate(other.refs())
{
}
MetaAggregate(MetaAggregate&& other)
requires(std::move_constructible<typename
Post #25150
12