Iteration over "zipped" tuples (for_each_in_tuples)
template&lt;typename T, typename... Ts&gt;
struct First { using Type = T; };

template&lt;typename... Ts&gt;
using First_t = typename First&lt;Ts...&gt;::Type;


This can be done using std::tuple_element in conjunction with std::tuple.

template &lt;typename... Ts&gt;
using First_t = std::tuple_element_t&lt;0, std::tuple&lt;Ts...&gt;&gt;;




template&lt;auto value, auto... values&gt;
inline constexpr auto all_same = (... &amp;&amp; (value == values));


For an empty pack, do you really want the value to be undefined?  The default behavior with empty packs are &amp;&amp; is considered true and || is considered false.



static_assert(sizeof...(Tuples) &gt; 0);


Instead of failing, maybe try to call the function with 0 arguments and see what happens?



static_assert(impl::all_same&lt;impl::tuple_size&lt;Tuples&gt;...&gt;);


If you want zipped-like behavior, you'll want to zip tuples until one of the tuples has exhausted its elements (min size instead first size).




  Is this implementation correct and can it be simplified?


Yes. Use an iterative approach, like sequential expansion, over recursion.


  Which name is better, for_each_in_tuple or for_each_in_tuples (or ...)?


Maybe for_each_zipped.  Tuples can be gathered from the signature of the function.



If you want to avoid the recursion, use fold expressions, std::index_sequence, and std::make_index_sequence.   

Start with a simple helper to just invoke the function with elements across all the tuples at a specific index.

template &lt;std::size_t Index, typename Function, typename... Tuples&gt;
constexpr void invoke_at(Function&amp;&amp; func, Tuples&amp;&amp;... tuples) {
    func(std::get&lt;Index&gt;(std::forward&lt;Tuples&gt;(tuples))...);
}


Now we need a way to sequentially call it (invoke_at&lt;0&gt;(args), invoke_at&lt;1&gt;(args), ..., invoke&lt;N&gt;(args)).  Use fold expressions like you did for all_same, but with the comma operator and unary right fold ((invoke_at&lt;N&gt;(args), ...)).  To generate the Ns that gets expanded, we use std::index_sequence.

template &lt;std::size_t... Indices, typename Function, typename... Tuples&gt;
constexpr void apply_sequence(Function&amp;&amp; func,  std::index_sequence&lt;Indices...&gt;, Tuples&amp;&amp;... tuples) {
    (((void)invoke_at&lt;Indices&gt;(std::forward&lt;Function&gt;(func), std::forward&lt;Tuples&gt;(tuples)...), ...));
}


Finally, write your function that does checks for preconditions, creates the index sequence, and forwards the arguments to the above helper.

template &lt;typename Function, typename... Tuples&gt;
constexpr void tuple_for_each(Function&amp;&amp; func, Tuples&amp;&amp;... tuples) {
    static_assert(sizeof...(tuples) &gt; 0, "Must be called with at least one tuple argument");

    constexpr auto min_length = std::min({std::tuple_size_v&lt;std::remove_reference_t&lt;Tuples&gt;&gt;...});
    if constexpr (min_length != 0) {
        impl::apply_sequence(std::forward&lt;Function&gt;(func),
                             std::make_index_sequence&lt;min_length&gt;{},
                             std::forward&lt;Tuples&gt;(tuples)...);
    }
    else {
        func();
    }
}


Note - The expansion has a cast to void that disables any overloaded shenanigans abusing the comma operator.