Search code examples
c++sfinaetemplate-specializationpartial-specialization

Is it possible to mix SFINAE and template specialisation?


Here is what I am roughly trying to achieve:

// the declaration
template<typename... Args>
struct ArgsEstimate;

// specialisation for string, SFINAE would be overkill
template<typename... Args>
struct ArgsEstimate<std::string&, Args...> {
    static const std::size_t size = 64 + ArgsEstimate<Args...>::size;
};

// specialisation for arithmetic types
template<typename AirthmeticT,
         typename std::enable_if<std::is_arithmetic<AirthmeticT>::value>::type* = nullptr,
         typename... Args>
struct ArgsEstimate<AirthmeticT, Args...> {
    static const std::size_t size = sizeof(AirthmeticT) + ArgsEstimate<Args...>::size;
};

// specialisation for pointer types
template<typename PtrT,
         typename std::enable_if<std::is_pointer<PtrT>::value>::type* = nullptr,
         typename... Args>
struct ArgsEstimate<PtrT, Args...> {
    static const std::size_t size = 32 + ArgsEstimate<Args...>::size;
};

The problem is that this code gives a compilation error "template parameters not deducible in partial specialization" at the points I have done enable_if. A static_assert inside the struct won't work either since there will be redefinition.

I know, I can probably do this with SFINAE and function overloading alone. However, for cases like just std::string, using SFINAE is an overkill.

So I was wondering if there is clean way of mixing template specialisation and SFINAE.


Solution

  • Direct answer to your question

    You can, but you really can't. Your case is complicated by variadic template arguments.

    // specialisation for arithmetic types
    template<class AirthmeticT, class... Args>
    struct ArgsEstimate<
        AirthmeticT,
        std::enable_if_t<std::is_arithmetic_v<AirthmeticT>>,
        Args...>
    {
        static const std::size_t size = sizeof(AirthmeticT) + ArgsEstimate<Args...>::size;
    };
    

    This works... sort of. You just need to make sure the second parameter is always void:

    ArgsEstimate<int, void, /* ... */> ok; // will use the integer specialization
    
    ArgsEstimate<int, int, int> wrong; // oups, will use the base template.
    

    This is impractical.

    C++20 concepts

    Concepts elegantly solve this:

    // specialisation for arithmetic types
    template<class T, class... Args>
        requires  std::is_arithmetic_v<T>
    struct ArgsEstimate<T, Args...>
    {
        static const std::size_t size = sizeof(T) + ArgsEstimate<Args...>::size;
    };
    

    The pre-concepts solution

    What you need to do is to split your class into two classes. One that defines the size just for 1 argument. Here you can use SFINAE. And the other one that summs them:

    template <class T, class Enable = void>
    struct ArgEstimate {};
    
    // specialisation for string, SFINAE would be overkill
    template<>
    struct ArgEstimate<std::string&>
    {
        static const std::size_t size = 64;
    };
    
    // specialisation for arithmetic types
    template<class T>
    struct ArgEstimate<T, std::enable_if_t<std::is_arithmetic_v<T>>>
    {
        static const std::size_t size = sizeof(T);
    };
    
    // specialisation for pointer types
    template <class T>
    struct ArgEstimate<T*>
    {
        static const std::size_t size = 32;
    };
    
    // the declaration
    template<class... Args> struct ArgsEstimate;
    
    template<class T>
    struct ArgsEstimate<T>
    {
        static const std::size_t size = ArgEstimate<T>::size;
    };
    
    template<class Head, class... Tail>
    struct ArgsEstimate<Head, Tail...>
    {
        static const std::size_t size = ArgEstimate<Head>::size + ArgsEstimate<Tail...>::size;
    };
    

    And if you have C++17 you can use fold expression to simplify the sum:

    template<class... Args>
    struct ArgsEstimate
    {
        static const std::size_t size = (... + ArgEstimate<Args>::size);
    };
    

    Also just wanted to point out that you don't need SFINAE for pointers:

    // specialisation for pointer types
    template <class T, class... Args>
    struct ArgsEstimate<T*, Args...> {
        static const std::size_t size = 32 + ArgsEstimate<Args...>::size;
    };