Search code examples
c++c++17compiler-optimizationvariadic-functionstemplate-meta-programming

api for indexed variadic arguments


I don't know if there is a good and clean way to index variadic arguments when unpacking tuple-like objects into callable handlers, i.e. when using std::apply.

Here is a not perfect, but rather clean solution:

    const auto animals = std::make_tuple("cow", "dog", "sheep");

    // handwritten, stateful, bad...
    std::apply([](const auto& ... str){
        const auto print = [](const auto& str, size_t index){
            std::cout << index << ": " << str << '\n';
        };

        // this should not be done by the user!!!
        size_t i = 0;
        (print(str, i++), ...);
    }, animals);

This solution is cleaner than using overloads with std::index_sequence since you don't have to write any code outside lambda's scope. Templates are not allowed within a block scope, so one would need to create some helper class outside.
It is bad, since there is a mutable state that is created by user. There should be no such thing, index should be available implicitly and on demand.

Here is what I think is desired and what I managed to implement so far:

    const auto animals = std::make_tuple("cow", "dog", "sheep");

    // desired
    // JavaScript-style destructuring. 
    // C++ structured bindings are not allowed as arguments
    // apply([](auto ... {value, index}){ ... }, animals);

    // still bad, but better - index is implicit and constant
    std::apply(indexed([](auto ... indexedValue){
        const auto print = [](const auto& indexedValue){
            const auto &[index, value] = indexedValue;
            std::cout << index << ": " << value << '\n';
        };
        (print(indexedValue), ...);
    }), animals);

C++ does not allow to have structured bindings as function arguments, and this is very unfortunate. Anyways, I consider this api better, than incrementing a counter by hand or writing some boilerplate helper.
All you have to do is to follow the damn train wrap your callable into indexed() function.
And it does not require any modifications on STL's part.

However, my implementation is very far from optimal. It results in far more instructions than the first example: https://godbolt.org/z/3G4doao39

Here is my implementation for indexed() function which I would like to be corrected.

#include <cstddef>
#include <type_traits>
#include <tuple>

namespace detail {
    template <size_t I, typename T>
    struct _indexed
    {
        constexpr static size_t index = I;
        T value;

        constexpr _indexed(std::integral_constant<size_t, I>, T t)
            : value(t)
        {}

        template <size_t Elem>
        friend constexpr auto get(const detail::_indexed<I, T>& v) noexcept -> 
                std::tuple_element_t<Elem, detail::_indexed<I, T>>{
            if constexpr (Elem == 0)
                return I;
            if constexpr (Elem == 1)
                return v.value;
        }

    };

    template <size_t I, typename T>
    _indexed(std::integral_constant<size_t, I>, T) -> _indexed<I, T>;

    template <typename CRTP>
    class _add_indices
    {
    public:
        template <typename ... Args>
        constexpr decltype(auto) operator()(Args &&... args) const noexcept {
            return (*this)(std::make_index_sequence<sizeof...(Args)>(), std::forward<Args>(args)...);
        }
    private:
        template <typename ... Args, size_t ... I>
        constexpr decltype(auto) operator()(std::index_sequence<I...>, Args ... args) const noexcept {
            // does not compile 
            // return std::invoke(&CRTP::callable, static_cast<CRTP const&>(*this), 
            //     _indexed(std::integral_constant<size_t, I>{}, std::forward<Args>(args))...);
            return static_cast<const CRTP&>(*this).callable(_indexed(std::integral_constant<size_t, I>{}, std::forward<Args>(args))...);
        }
    };
}

template <size_t I, typename T>
struct std::tuple_size<detail::_indexed<I, T>> : std::integral_constant<size_t, 2> {};

template <size_t I, typename T>
struct std::tuple_element<0, detail::_indexed<I, T>>
{
    using type = size_t;
};

template <size_t I, typename T>
struct std::tuple_element<1, detail::_indexed<I, T>>
{
    using type = T;
};

template <typename Callable>
constexpr auto indexed(Callable c) noexcept{
    struct _c : detail::_add_indices<_c> {
        Callable callable;
    };

    return _c{.callable = c};
}

// api:
// apply(indexed([](auto ... indexedValue){}), tuple);

Solution

  • If I correctly understand what do you want... it seems to me that you only need a class/struct as follows

    template <typename Callable>
    struct indexed_call
    {
      Callable c;
    
      template <std::size_t ... Is, typename ... As>
      constexpr auto call (std::index_sequence<Is...>, As && ... as) const {
        return c(std::pair{Is, std::forward<As>(as)}...);
      }
    
      template <typename ... As>
      constexpr auto operator() (As && ... as) const {
        return call(std::index_sequence_for<As...>{}, std::forward<As>(as)...);
      }
    };
    

    and an explicit, trivial, deduction guide

    template <typename C>
    indexed_call(C) -> indexed_call<C>;
    

    and the call change a little as follows

    std::apply(indexed_call{[](auto ... indexedValue){
       const auto print = [](const auto& iV){
          const auto &[index, value] = iV;
          std::cout << index << ": " << value << '\n';
       };
       (print(indexedValue), ...);
    }}, animals);
    

    or, if you prefer, also the call can be simplified

    std::apply(indexed_call{[](auto ... iV){
       ((std::cout << iV.first << ": " << iV.second << '\n'), ...);
    }}, animals);
    

    The following is a full compiling example

    #include <type_traits>
    #include <iostream>
    #include <tuple>
    
    template <typename Callable>
    struct indexed_call
    {
      Callable c;
    
      template <std::size_t ... Is, typename ... As>
      constexpr auto call (std::index_sequence<Is...>, As && ... as) const {
        return c(std::pair{Is, std::forward<As>(as)}...);
      }
    
      template <typename ... As>
      constexpr auto operator() (As && ... as) const {
        return call(std::index_sequence_for<As...>{}, std::forward<As>(as)...);
      }
    };
    
    template <typename C>
    indexed_call(C) -> indexed_call<C>;
    
    int main(){
        const auto animals = std::make_tuple("cow", "dog", "sheep");
    
        std::apply(indexed_call{[](auto ... indexedValue){
           const auto print = [](const auto& iV){
              const auto &[index, value] = iV;
              std::cout << index << ": " << value << '\n';
           };
           (print(indexedValue), ...);
        }}, animals);
    
        std::apply(indexed_call{[](auto ... iV){
           ((std::cout << iV.first << ": " << iV.second << '\n'), ...);
        }}, animals);
    }
    

    ----- EDIT -----

    The OP writes

    I don't think that utilizing std::pair is semantically correct. Would be better to have .value, .index

    I usually prefer to use standard components, when functional equivalents, but if you want something with a value and a index components, you can add a simple struct (name it as you prefer)

    template <typename V>
    struct val_with_index
    {
      std::size_t  index;
      V            value;
    };
    

    and another trivial explicit deduction guide

    template <typename V>
    val_with_index(std::size_t, V) -> val_with_index<V>;
    

    then you have to modify the call() method to use it instead std::pair

    template <std::size_t ... Is, typename ... As>
    constexpr auto call (std::index_sequence<Is...>, As && ... as) const {
      return c(val_with_index{Is, std::forward<As>(as)}...);
    } // ......^^^^^^^^^^^^^^
    

    and now works for the double lambda case

    For the simplified case, obviously you have to change first and second with index and value

    std::apply(indexed_call{[](auto ... iV){
       ((std::cout << iV.index << ": " << iV.value << '\n'), ...);
    }}, animals); // ....^^^^^...............^^^^^
    

    Again the full compiling example

    #include <type_traits>
    #include <iostream>
    #include <tuple>
    
    template <typename V>
    struct val_with_index
    {
      std::size_t  index;
      V            value;
    };
    
    template <typename V>
    val_with_index(std::size_t, V) -> val_with_index<V>;
    
    template <typename Callable>
    struct indexed_call
    {
      Callable c;
    
      template <std::size_t ... Is, typename ... As>
      constexpr auto call (std::index_sequence<Is...>, As && ... as) const {
        return c(val_with_index{Is, std::forward<As>(as)}...);
      }
    
      template <typename ... As>
      constexpr auto operator() (As && ... as) const {
        return call(std::index_sequence_for<As...>{}, std::forward<As>(as)...);
      }
    };
    
    template <typename C>
    indexed_call(C) -> indexed_call<C>;
    
    int main(){
        const auto animals = std::make_tuple("cow", "dog", "sheep");
    
        std::apply(indexed_call{[](auto ... indexedValue){
           const auto print = [](const auto& iV){
              const auto &[index, value] = iV;
              std::cout << index << ": " << value << '\n';
           };
           (print(indexedValue), ...);
        }}, animals);
    
        std::apply(indexed_call{[](auto ... iV){
           ((std::cout << iV.index << ": " << iV.value << '\n'), ...);
        }}, animals);
    }