c++templatesc++20applyc++23

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:

Thanks!


Solution

  • 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;
    }