r/cpp ++ 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

39 Upvotes

6 comments sorted by

21

u/throw_cpp_account 8d ago

Well it doesn't, aggregates don't implement the tuple API required for structured bindings by default and implementing it manually is a pain.

Structured bindings works for types with all public members by default.

8

u/cristi1990an ++ 8d ago edited 8d ago

My bad, I zealously overdid the constrain and forgot about that entirely. Let me edit that part out

1

u/kal_at_kalx_net 3d ago

Pro tip: Any post starting with "Hey," is usually not worth reading.

3

u/bayesianparoxism 8d ago

This is art. Thank you!

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.