Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apply a templated lambda to each type in a std::tuple (C++20)

I've run into an interesting C++20 problem where I want to apply the same templated function to each type listed in a std::tuple. Here's some pseudocode to illustrate the idea:

template <typename T>
void logType()
{
    std::cout << "Type: " << typeid(T).name() << std::endl;
}

int main()
{
    using MyTypes = std::tuple<float, int, std::string>;
    tapply<MyTypes>([]<typename T>() {
        logType<T>();
    });
}

So after a few unsuccessful attempts I believe I've found the ugly way to do so in C++20:

template <typename Tuple, typename Func>
void tapply(Func&& func)
{
   static constexpr Tuple defaultTuple{};
   // Apply the tuple to the callable function
   std::apply ([&func](auto&&... args) {
      (func.template operator() < std::decay_t<decltype(args)> > (), ...);
   }, defaultTuple);
}

And this is how the example below would concretely looks like:

void main()
{
   using MyTypes = std::tuple<float, int, std::string>;
   tapply<MyTypes>([] <typename T>() {
      logType<T> ();
   });
}

So far this seems to work like a charm in unit tests but I'm surprised there is no standard way of doing this in the standard library. Somehow this feels like almost the most standard use case where I would use the new templated lambda function available in C++20.

So I've got two questions for the template addicts:

  • Am I missing an existing method of the standard library that would already exhibit the same feature? It seems that std::apply will be able to do this starting from C++23
  • Would you recommend a more elegant / standard implementation compatible with C++20?

Thanks!

like image 753
user14170 Avatar asked May 16 '26 21:05

user14170


2 Answers

Am I missing an existing method of the standard library that would already exhibit the same feature? It seems that std::apply will be able to do this starting from C++23.

I don't think so. From my reading, std::apply c++23 won't help more (it allows custom tuples)

Would you recommend a more elegant / standard implementation compatible with C++20?

Not sure which part you consider ugly,

I would do something like:

template <typename Tuple, typename Func>
void tapply(Func&& func)
{
  []<typename...Ts>(std::type_identity<std::tuple<Ts...>>)
  {
    (func.template operator()<Ts>(), ...);
  }(std::type_identity<Tuple>{});
}

which allows non-default constructible types.

If you want to support tuple-like type (as std::pair/std::array), std::index_sequence should replace some part of above code.

To avoid func.template operator()<Ts> syntax, I tend to have deducible template parameters, as using std::type_identity for types, and std::integral_constant for non-type template parameters.

Something like

template <typename Tuple, typename Func>
void tapply(Func&& func)
{
  []<typename...Ts>(std::type_identity<std::tuple<Ts...>>)
  {
    (func(std::type_identity<Ts>{}), ...);
  }(std::type_identity<Tuple>{});
}

with usage

int main()
{
   using MyTypes = std::tuple<float, int, std::string>;

   tapply<MyTypes>([] <typename T>(std::type_identity<T>)) {
      logType<T>();
   });
   // or
   tapply<MyTypes>([](auto type)) {
      logType<typename decltype(type)::type>();
   });

   return 0;
}
like image 51
Jarod42 Avatar answered May 19 '26 12:05

Jarod42


If the goal is to do this:

using MyTypes = std::tuple<float, int, std::string>;
tapply<MyTypes>([] <typename T>() {
    logType<T> ();
});

Then I would not use std::tuple as the type-list, and also not use std::apply. It can be made to work, but it's compile-time inefficient.

Boost.Mp11 is a great go-to for metaprogramming needs. In this case, there is nearly an algorithm for this: mp_for_each. Except that mp_for_each<L>(f) calls f(T()) for each element T in the list, rather than doing f.operator<T>(). But we can achieve that with a pretty simple wrapper:

template <class L, class F>
auto mp_for_each_type(F&& f) -> void {
  mp_for_each<mp_transform<mp_identity, L>>([&]<class T>(mp_identity<T>){
    f.template operator<T>();
  });
}

The use of mp_identity here is to avoid having to actually construct any of the types in L. add_pointer_t would accomplish the same thing, except that it wouldn't work for all types (e.g. reference types), so use the thing that always works.

This does exactly what you want with tapply (I wouldn't name it tapply since it doesn't actually resemble std::apply, you're just iterating over all the types one by one) with the added benefit that it actually works on all type-lists, not just std::tuple (and std::pair).

like image 37
Barry Avatar answered May 19 '26 12:05

Barry



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!