c++c++14sfinaetemplate-specializationparameter-pack

Partial template specialization for when all template parameters are the same type


Is it possible in C++14 (so no constraints, require or fold expressions, but there is SFINAE) to have a partial template specialization of a class template

template <typename ...T>
class Generic {
    using Container = std::tuple<T...>;
};

for the case when every template parameter is the same type? I need this because the generic version would use an std::tuple container, while the specialization would use an std::array (and not std::vector, because the code will run in an embedded environment, so I'd like to avoid heap allocations).

For example Generic<int,float,char>::Container should be std::tuple<int,float,char>, while Generic<int,int,int>::Container should be std::array<int,3>.


Solution

  • It's not hard to do with C++14 only features. Since that standard relaxed a bunch of constraints on constant evaluation, we can even do it in a fairly readable way.

    Just need a few helpers:

    #include <type_traits>
    #include <tuple>
    #include <array>
    
    namespace detail {
        template<typename...>
        struct head_impl;
    
        template<typename H, typename ...Rest>
        struct head_impl<H, Rest...> { using type = H; };
    
        template<typename ...Ts>
        using head = typename head_impl<Ts...>::type;
    
        template<typename ...Ts>
        inline constexpr bool test_same() {
            using T = head<Ts...>;
    
            for(bool b : { std::is_same<T, Ts>::value... }) {
                if (!b)
                    return false;
            }
    
            return true;
        }
    
        template<>
        inline constexpr bool test_same<>() { return true; }
    }
    
    template <typename ...T>
    class Generic {
        using Container = std::conditional_t<
                            detail::test_same<T...>(),
                            std::array<detail::head<T...>, sizeof...(T)>,
                            std::tuple<T...>
                          >;
    };
    

    The crux of the work is in the detail namespace. We write the predicate as a constexpr function, and rely on std::conditional_t to choose a container type. Since in C++14 contexpr functions aren't limited to a single return statement, we can just convert the type pack into an initializer_list<bool> that we immediately iterate. Each item in the list is the result of checking if we have the same type in the pack in that position, as we have in the head.

    Now, I didn't use specialization like you asked and that's primarily because it's tricky to do without rewriting the primary or adding delegation, and I assumed only the container matters. If that's not the case, you can still reuse the utilities and do something like this

    template <bool, typename ...T>
    class Generic_Base;
    
    template <typename ...T>
    class Generic_Base<false, T...> {
        using Container = std::tuple<T...>;
    };
    
    template <typename ...T>
    class Generic_Base<true, T...> {
        using Container = std::array<detail::head<T...>, sizeof...(T)>;
    };
    
    template <typename ...T>
    class Generic : public Generic_Base<detail::test_same<T...>(), T...> {
    
    };
    

    Specialize the bases as much as needed, and pull the correct base into Generic (which can also have common members).