Why 'enable_if' cannot be used to disable this declaration here
Asked Answered
L

2

5
#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?

Lemon answered 6/6, 2018 at 6:36 Comment(2)
Are you familiar with SFINAE? The existing answer isn't bad per se, but it doesn't explain SFINAE. Yet I'd think that if you understand SFINAE, the question would be already answered.Bakken
@MSalters: I am familiar with SFINAE, apparently not very well :) but thankfully, enough to understand the answer given here and explore further.Lemon
G
8

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);
}
Grassy answered 6/6, 2018 at 7:5 Comment(4)
Why I need such overload?: If you look at my code above between the two functions first one void foo(Args&&... args) will work with rvalue referenses and the second one void foo(const Args&... args) with rest. The only time they conflict is when C is instantiated with no parameter. Basically, I wanted to achieve something like perfect forwarding with class template arguments.Lemon
Thank you so much.Lemon
"SFINAE only works for deduced template arguments" - is this really true? Afaik SFINAE also works fine applied to explicitly specified template arguments as well as default template arguments, none of which are obtained as deduced template arguments. Rather, which applies also in OP's case, the enable_if construct needs to contain some template argument from the entity upon which is applied, otherwise the enable_if construct itself will never take part of the template parameter substitutation (where template argument deduction is one part) for that entity, in which case SFINAE fails.Hinda
And indeed, SFINAE does emphasize on substitutation (SFINAE) and not deduction (DFINAE?).Hinda
R
6

As better explained by Klaus, your original code doesn't works because std::enable_if_t need to check a template of the method itself and isn't enough the template list of the class.

I propose a simplified alternative of the Klaus's solution.

First of all, you need a template parameter to check; you can use one with a default value deduced from the template paramenter of the class (Args...).

Surely you can use a type taking a std::tuple of the Args... but, taking in count that you're only interested in the number of Args... parameters, I find it's simpler to use a std::size_t template parameter initialized with the number of Args...

template <std::size_t N = sizeof...(Args)>
std::enable_if_t<N> foo (Args const & ... args)
 { std::cout << "N args" << std::endl; }  

Regarding the zero args version, there is no need to make it a template version; you can simply write it with zero parameters

void foo ()
 { std::cout << "zero args" << std::endl; }

In case of zero Args..., the not-template version take the precedence over the template one.

The following is a full compiling example

#include <iostream>
#include <type_traits>

template <typename... Args>
struct C
 {
   void foo ()
    { std::cout << "zero args" << std::endl; }

   template <std::size_t N = sizeof...(Args)>
   std::enable_if_t<N> foo(const Args&... args)
    { std::cout << "N args" << std::endl; }
};

int main ()
 {
   C<>    c0;
   C<int> c1;
   c0.foo();
   c1.foo(42);
 }
Rico answered 6/6, 2018 at 11:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.