lock3 / meta

122 stars 11 forks source link

How to write tuple constructors? #294

Open rbock opened 3 years ago

rbock commented 3 years ago

Hi,

I am having a hard time writing tuple constructors using reflection and/or metaprogramming. With metaprogramming, you can imagine structs as

template<typename... Ts>
struct tuple
{
   // ... some operators

   // And then data members that are generated as something like
   T0 m0;
   T1 m1;
   // ...
};

How would I write the variadic constructors? For instance

   template<typename... Us>
   constexpr tuple(Us&&... args);

I was thinking of constructing something like

inline constexpr std::size_t index[3] = {0, 1, 2};

And then use that to generate something like

  template <typename A0, typename A1, typename A2>
  constexpr Tuple(A0&& a0, A1&& a1, A2&& a2)
      : m0(std::forward<A0>(a0)),
        m1(std::forward<A1>(a1)),
        m2(std::forward<A2>(a2)) {}

I believe this should be possible. But I currently fail to see how to do it?

I cannot use fragments to create such lists as far as I can tell. And I might miss something when trying to use pack expansion...

Cheers,

Roland

DarkArc commented 3 years ago

I believe the pack splicing stuff we're working on will help with this; I'll try to follow up when that's ready.

rbock commented 3 years ago

Thanks, looking forward to it!

groundswellaudio commented 3 years ago

Bit late to this, but in theory this should already be possible by using a good old sequence expansion, like :

template <class T>
consteval meta::info refl(){
    return reflexpr(T);
}

template <std::size_t... Idx>
consteval meta::info make_ctor(std::integer_sequence<std::size_t, Idx...>){
    return fragment struct {
        template <class... Args>
        tuple(Args&&... args) : unqualid("mem_", Idx){args}... {} // parallel expansion 
    };
}

template <class... Ts>
struct tuple {

    consteval {
        constexpr std::array<meta::info, sizeof...(Ts)> members = {refl<Ts>()...};

        for (unsigned k = 0; k < sizeof...(Ts); ++k){
            -> fragment struct {
                typename (%{members[k]}) unqualid("mem_", %{k});
            };
        }

        -> make_ctor(std::make_index_sequence<sizeof...(Ts)>{});
    }
};

In practice however this doesn't compiles, I think constructors injection aren't supported yet? (funnily enough, this is exactly how the constructors of tuples implemented with variadic inheritance looks like : just a double pack expansion).

DarkArc commented 3 years ago

There are a few problems there, but (I think) this should work (looks like we have a bug with checking member-initializers too soon):

template <class T>
consteval meta::info refl(){
    return reflexpr(T);
}

template <std::size_t... Idx>
consteval meta::info make_ctor(std::integer_sequence<std::size_t, Idx...>){
    return fragment struct tuple {
        template <class... Args>
        tuple(Args&&... args) : unqualid("mem_", Idx){args}... {} // parallel expansion 
    };
}

template <class... Ts>
struct tuple {
    consteval {
        constexpr std::array<meta::info, sizeof...(Ts)> members = {refl<Ts>()...};

        for (unsigned k = 0; k < sizeof...(Ts); ++k){
            -> fragment struct {
                typename (%{members[k]}) unqualid("mem_", %{k});
            };
        }
    }

    consteval -> make_ctor(std::make_index_sequence<sizeof...(Ts)>{});
};
groundswellaudio commented 3 years ago

Neat.

Also @rbock I just realized something : for this particular case you don't need a constructor at all. A tuple implemented this way is just a plain old aggregate, so there is no need for the dozen of constructors of std::tuple anymore :) (I also tried to make a tuple constructor at some point and this is just hitting me now... pretty funny how we can get anchored in our ways)

rbock commented 3 years ago

@groundswellaudio Good point, but this one still needs to be a constructor, and I guess it is not that different to write?

template <class... UTypes>
tuple( tuple<UTypes...>&& other );
groundswellaudio commented 3 years ago

Yes, it could be implementable in the same way. But if you declare a constructor, you lose the ability to treat the type as an aggregate and emplace members easily (i wish we had some better mechanism for that). I think a better way to do this would be to implement a conversion operator, also using a pack expansion :

template <class... Us, std::size_t... Idx>
consteval meta::info make_conversion_op(std::integer_sequence<std::size_t, Idx...>){
    return fragment this {
        return { static_cast<Us>( unqualid("mem_", Idx) )... };
    };
}

template <class... Ts>
struct tuple {

    static constexpr std::array<meta::info, sizeof...(Ts)> members = {refl<Ts>()...};

    consteval {
        for (unsigned k = 0; k < sizeof...(Ts); ++k){
            -> fragment struct {
                typename (%{members[k]}) unqualid("mem_", %{k});
            };
        }
    }

    template <class... Us>
    operator tuple<Us...>(){
        consteval {
            -> make_conversion_op<Us...>(std::make_index_sequence<sizeof...(Ts)>{});
        }
    }
};

This currently crash the compiler on the P1240r2 trunk on godbolt ("error: cannot compile this scalar expression yet") however, but i think the logic is sound. It comes with a few caveats about bracket initialization syntax, but there are some workarounds.

rbock commented 3 years ago

@groundswellaudio I just noticed that I never answered. Using a conversion operator instead certainly is a nice idea.

I am anxiously waiting for new iterations on the new syntax (ideally with metaprogramming) to experiment with :-)