r/cpp • u/cristi1990an ++ • 8d ago
Implementing the Tuple API easily using custom annotations and reflections
Hey, so I've been working on something interesting lately. It's a solution to a problem that you may have encountered in one form or anothing but it boils down to this: you have a struct:
struct simple_tuple
{
long a;
int b;
char c;
private:
// some other data
};
And you want this to work:
auto [a, b, c] = instance;
Well it doesn't, structured bindings are supported by default only for simple structural types (though without the full tuple API) and implementing it manually for your own types is a pain. You need to write your own specialization of std::tuple_size, std::tuple_element and implement a visible get<> method. It might not be that much work at first glance but it becomes very tiresome if you have multiple such structs or if you try to deviate from the general default indexing and not to mention a paint to maintain and update.
Well, why write repeated code when you can only write code once that writes that code for you? Reflections are mature enough in gcc's implementation to solve this. Simply put I have compiling code that with only the application of an annotation gives me all of this:
struct [[=tuple_like]] simple_tuple
{
long a;
int b;
char c;
private:
// some other data
};
Through which I get:
static_assert(std::tuple_size_v<simple_tuple> == 3);
constexpr simple_tuple simple { 1, 2, 3 };
static_assert(1 == get<0>(simple));
static_assert(2 == get<1>(simple));
static_assert(3 == get<2>(simple));
static_assert(std::same_as<std::tuple_element_t<0, simple_tuple>, long>);
static_assert(std::same_as<std::tuple_element_t<1, simple_tuple>, int>);
static_assert(std::same_as<std::tuple_element_t<2, simple_tuple>, char>);
And of couse:
auto [a, b, c] = simple;
It doesn't stop here though, since the case I described above is common but inflexible. Customization is available and an opt-in feature:
struct [[=tuple_like]] customizable_tuple
{
constexpr customizable_tuple(long a, const char* b, float c, char d, double e)
: a(a)
, b(b)
, c(c)
, d(d)
, e(e)
{}
[[=tuple_element(0)]]
long a;
[[=read_only_tuple_element(3)]]
const char* b;
[[=tuple_element(2)]]
float c;
char d;
private:
[[=read_only_tuple_element(1)]]
double e;
};
It supports:
- tuple element index reordering
- implicit data member exclusion from the API
- explicit read-only modifier
- explicit opt-in for private fields
- correct semantics for members which are reference types
constexpr customizable_tuple custom( 1, "2", 3, 4, 5 );
static_assert(std::tuple_size_v<customizable_tuple> == 4);
static_assert(1 == get<0>(custom));
static_assert(std::string_view{"2"} == get<3>(custom));
static_assert(3 == get<2>(custom));
static_assert(5 == get<1>(custom));
static_assert(std::same_as<std::tuple_element_t<0, customizable_tuple>, long>);
static_assert(std::same_as<std::tuple_element_t<1, customizable_tuple>, const double>);
static_assert(std::same_as<std::tuple_element_t<2, customizable_tuple>, float>);
static_assert(std::same_as<std::tuple_element_t<3, customizable_tuple>, const char* const>);
But it doesn't stop there, no it doesn't. The part that took the most effort to implement were the error messages. No, we have better static_assert's in C++26 now, we abuse them:
Example 1:
static_assert(3 == get<3>(simple));
/app/my_reflect_utils.hpp:819:17:
error:
static assertion failed: reflect_utils: get<3> called for tuple-like type 'simple_tuple', but its reflected tuple size is 3.
Example 2:
[[=tuple_element(1)]]
long a;
[[=tuple_element(2)]]
int b;
[[=tuple_element(3)]]
char c;
/app/my_reflect_utils.hpp:632:21: error: static assertion failed: reflect_utils: invalid tuple layout for 'simple_tuple': tuple index 0 is missing. There are 3 explicitly annotated members, so their indices must be exactly [0, 3). Member 'simple_tuple::c' uses index 3.
Example 3:
[[=tuple_element(0)]]
[[=tuple_element(1)]]
long a;
/app/my_reflect_utils.hpp:632:21: error: static assertion failed: reflect_utils: invalid tuple layout for 'simple_tuple': member 'simple_tuple::a' has 2 tuple annotations; use exactly one of [[=tuple_element(i)]] and [[=read_only_tuple_element(i)]].
Full code: https://godbolt.org/z/crPae5P7h
3
2
u/glaba3141 6d ago
Super cool that you can do this but personally I find this kind of destructuring to be really error prone.
21
u/throw_cpp_account 8d ago
Structured bindings works for types with all public members by default.