Home > Blockchain >  variadic template: SFINAE on last argument
variadic template: SFINAE on last argument

Time:03-11

I have an array (of any rank), and I would like to have an index operator that:

  1. Allows for missing indices, such that the following is equivalent

    a(1, 0, 0, 0);
    a(1, my::missing);
    

    This in itself if straightforward (see example implementation below): one just recursively adds arg * strides[dim] until my::missing it hit.

  2. Allows automatic prepending of zeros, such that the following is equivalent

    a(0, 0, 0, 1);
    a(1);
    

    Also this is not hard (see example implementation below): one recursively adds arg * strides[dim offset].

What I cannot get my head around is: How to combine the two? The implementation of 2. makes me start of the wrong foot for 1. (I'm limited to <= C 14)

Example implementation of "my::missing" without auto pre-pending zeros

enum class my { missing };

template <size_t dim, class S>
inline size_t index_impl(const S&) noexcept
{
    return 0;
}

template <size_t dim, class S, class... Args>
inline size_t index_impl(const S& strides, enum my arg, Args... args) noexcept
{
    return 0;
}

template <size_t dim, class S, class Arg, class... Args>
inline size_t index_impl(const S& strides, Arg arg, Args... args) noexcept
{
    return arg * strides[dim]   index_impl<dim   1>(strides, args...);
}

template <class S, class Arg, class... Args>
inline size_t index(const S& strides, Arg arg, Args... args)
{
    return index_impl<0>(strides, arg, args...);
}

int main()
{
    std::vector<size_t> strides = {8, 4, 2 ,1};
    std::cout << index(strides, 1, 2, 0, 0) << std::endl;
    std::cout << index(strides, 1, 2, my::missing) << std::endl;
}

Example implementation of prepending zeros without "my::missing"

template <size_t dim, class S>
inline size_t index_impl(const S&) noexcept
{
    return 0;
}

template <size_t dim, class S, class Arg, class... Args>
inline size_t index_impl(const S& strides, Arg arg, Args... args) noexcept
{
    return arg * strides[dim]   index_impl<dim   1>(strides, args...);
}

template <class S, class Arg, class... Args>
inline size_t index(const S& strides, Arg arg, Args... args)
{
    constexpr size_t nargs = sizeof...(Args)   1;
    if (nargs == strides.size())
    {
        return index_impl<0>(strides, arg, args...);
    }
    else if (nargs < strides.size())
    {
        return index_impl<0>(strides.cend() - nargs, arg, args...);
    }
    return index_impl<0>(strides, arg, args...);
}

int main()
{
    std::vector<size_t> strides = {8, 4, 2 ,1};
    std::cout << index(strides, 1, 2) << std::endl;
    std::cout << index(strides, 0, 0, 1, 2) << std::endl;
}

CodePudding user response:

Here is how you can fork implementation for both scenarios:

template<typename T, typename ...Ts>
struct last_type_helper
{
    using type = typename last_type_helper<Ts...>::type;
};

template<typename T>
struct last_type_helper<T>
{
    using type = T;
};

template<typename... Ts>
using last_type = typename last_type_helper<Ts...>::type;

enum class my { missing };
template<typename ...Ts>
constexpr bool LastTypeIsMy = std::is_same<my, last_type<Ts...>>::value;

template <size_t dim, class S, class Arg, class... Args>
inline size_t head_index(const S& strides, Arg arg, Args... args) noexcept
{
    // details of implemetion left for you
    return 0;
}

template <size_t dim, class S, class Arg, class... Args>
inline size_t tail_index(const S& strides, Arg arg, Args... args) noexcept
{
    // details of implemetion left for you
    return 1;
}

template <class S, class... Args>
size_t index(const S& strides, Args... args)
{
    return LastTypeIsMy<Args...> ? tail_index<0>(strides, args...) : head_index<0>(strides, args...);
}

https://godbolt.org/z/5qroKzn6c

Details of implementation I'm leaving to you (I just solved your main problem).

CodePudding user response:

C 14 distinctively lacks fold expressions, which usually makes parameter packs much harder to work with. Thankfully, we can use array initializations to fake fold expressions, which is allowed in constexpr functions, so we can get away with no recursion and better code readability.

template<typename... Idxs>
constexpr int indexing_type()
{
    size_t integrals = 0;
    bool unused[] = {(integrals  = std::is_integral<Idxs>::value, false)...};
    (void)unused;
    bool mys[] = {std::is_same<my, Idxs>::value...};
    bool last = mys[sizeof(mys) - 1];
    if(integrals == sizeof...(Idxs))
        return 1;
    if(integrals == sizeof...(Idxs) - 1 && last)
        return 2;
    return 0;
}

template<typename S, size_t... Is, typename... Idxs>
inline auto mul_reduce(const S& strides, size_t off, std::index_sequence<Is...>, Idxs... idxs)
{
    size_t sum = 0;
    bool unused[] = {(sum  = strides[off   Is] * size_t(idxs), false)...};
    (void)unused;
    return sum;
}

template<typename S, typename... Idxs>
inline auto index(const S& strides, Idxs... idxs)
-> std::enable_if_t<indexing_type<Idxs...>() == 1, size_t>
{
    auto off = strides.size() - sizeof...(Idxs);
    return mul_reduce(strides, off, std::make_index_sequence<sizeof...(Idxs)>{}, idxs...);
}

template<typename S, typename... Idxs>
inline auto index(const S& strides, Idxs... idxs)
-> std::enable_if_t<indexing_type<Idxs...>() == 2, size_t>
{
    return mul_reduce(strides, 0, std::make_index_sequence<sizeof...(Idxs)>{}, idxs...);
}

indexing_type tells you which function to dispatch to, and also disables index from overload resolution in case the parameter types are wrong.

In mul_reduce, we abuse the fact that size_t(missing) == 0 to avoid needing to remove it from the end.

  •  Tags:  
  • c
  • Related