Search code examples
c++c++14variadic-templatestype-traitsenable-if

Why 'enable_if' cannot be used to disable this declaration here


#include<string>
#include<type_traits>

template<typename... Args>
class C {
public:
    void foo(Args&&... args) {      

    }

    template<typename = std::enable_if_t<(0 < sizeof...(Args))>>
    void foo(const Args&... args) {     

    }
};

int main() {
    C<> c;
    c.foo();
    return 0;
}

Above code works as expacted (by me :)) and calls void foo(Args&&... args) at run-time in msvc 2015 but same code fails to even compile in both gcc 7.3 and clang 6.0.0 with error:

error: no type named 'type' in 'std::enable_if'; 'enable_if' cannot be used to disable this declaration

I want to understand what is wrong with the above code and how can it be fixed?


Solution

  • SFINAE only works for deduced template arguments. In your case your method did not depend on any parameter from the method call so it is not in a deduced context. Everything is already known while instantiate the class itself.

    MSVC is simply wrong in that case.

    Workaround:

    template<typename... Args>
    class C
    {
        public:
            template< typename U = std::tuple<Args...>>
                std::enable_if_t< (std::tuple_size<U>::value > 0 ) > foo(const Args&...)
                {
                    std::cout << "Args  > 0 type " << std::endl;
                }
    
            template< typename U = std::tuple<Args...>>
                std::enable_if_t< (std::tuple_size<U>::value == 0)> foo(const Args&...)
                {
                    std::cout << "Args 0 type " << std::endl;
                }
    };
    
    int main()
    {
        C<>{}.foo();
        C<int>{}.foo(1);
    }
    

    I don't know why you need such a overload, because if the parameter list is empty, you simply should write an overload for such without any SFINAE stuff at all.

    if your compiler is not outdated ( c++14 only ) it is much easier with using constexpr if:

    template <typename... Args>
    struct C
    {
        void foo (const Args&... args)
        {
            if constexpr ( sizeof...(args) == 0)
            {
                std::cout << "0" << std::endl;
            }
            else
            {
                std::cout << ">0" << std::endl;
            }
        }
    };
    
    int main ()
    {
        C<>    c0;
        C<int> c1;
        c0.foo();
        c1.foo(42);
    }
    

    EDIT after comment:

    To avoid SFINAE you can also use specialized template classes like this:

    // provide common stuff here
    template <typename ... ARGS>
    class CAll { protected: void DoSomeThing(){ std::cout << "Do some thing" << std::endl; } };
    
    template<typename ... ARGS>
    class C;
    
    // special for no args
    template<>
    class C<>: public CAll<>
    {   
        public:
            void foo() 
            {
                std::cout << "none" << std::endl; 
                this->DoSomeThing();
            }   
    };  
    
    //special for at minimum one arg
    template<typename FIRST, typename ... REST>
    class C<FIRST, REST...>: public CAll<FIRST, REST...>
    {   
        public:
            void foo( FIRST&, REST&... )
            {   
                std::cout << "lvalue" << std::endl;
                this->DoSomeThing();
            }
    
            void foo( FIRST&&, REST&&... )
            {   
                std::cout << "rvalue" << std::endl;
                this->DoSomeThing();
            }   
    };  
    
    int main()
    {   
        int a;
        C<>{}.foo();
        C<int>{}.foo(1);
        C<int>{}.foo(a);
    }