Home > front end >  SFINAE Ambiguity
SFINAE Ambiguity

Time:12-30

Looking to get some detailed explanations as to why the following is allowed / disallowed.

#include <type_traits>
#include <iostream>

template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>> foo(T v) {
    std::cout << "arithmetic version called\n";
}

template<typename T>
std::enable_if_t<!std::is_arithmetic_v<T>> foo(T v) {
    std::cout << "non arithmetic version called\n";
}

int main() {
    foo(33);
    foo("hello");
}

In relation to the above code sample, why is this allowed as overload resolution would not kick in as the functions differ only by return type?

Following on from the above, with Expression SFINAE I have to make sure the i disambiguate the overloads as in the following code snippet.

#include <type_traits>
#include <iostream>

template<typename T, std::enable_if_t<std::is_arithmetic_v<T>>* p = nullptr>
void foo(T v) {
    std::cout << "arithmetic version called\n";
}
    
template<typename T, std::enable_if_t<!std::is_arithmetic_v<T>>* p = nullptr>
void foo(T v) {
    std::cout << "non arithmetic version called\n";
}


int main() {
    foo(33);
    foo("hello");
}

So I guess this question is more of a why it works, ie language rules vs how to do it. I have been using these techniques for a while and often still scratch my head on which bits of the languages rules enable this.

Thanks in advance.

#include <type_traits>
#include <iostream>
#include <string>


template<typename T>
auto increment_or_self_(T a, int i) -> std::decay_t<decltype(  std::declval<T&>(), std::declval<T>())> {
      a;
    return a;
}

template<typename T>
auto increment_or_self_(T a, ...) -> T {
    return a;
}

template<typename T>
T increment_or_self(T a) {
    return increment_or_self_(a, 0);
}

int main() {

    std::cout << increment_or_self(33) << std::endl;
    std::cout << increment_or_self(std::string("hello")) << std::endl;
}

In the above increment_or_self has a dummy arg to ensure the overloads are not ambiguous.

Also why do we have to use pointers when we move the EnableIf as shown in the following?

CodePudding user response:

Your first example has no problems with overload resolution, because you do not have two times the same function with different return types, because the SFINAE stuff already disables one of both in each of the instantiations. Even if your SFINAE expression will result in two different return types, it simply doesn't matter, because both instantiated functions will have different signatures as they have different input parameters.

I modified your example and this will also be well formed ( see different return types with this change )

std::enable_if_t<std::is_arithmetic_v<T>, float> foo(T ) { 
    std::cout << "arithmetic version called\n";
    return 1.1;
}

template<typename T>
std::enable_if_t<!std::is_arithmetic_v<T>, int> foo(T ) { 
    std::cout << "non arithmetic version called\n";
    return 1;
}

int main() {
    foo(33);
    foo("hello");

    std::cout << std::is_same_v< float, decltype(foo(33))> << std::endl; 
    std::cout << std::is_same_v< int, decltype(foo("Hallo"))> << std::endl; 
}

Your question: "Also why do we have to use pointers when we move the EnableIf as shown in the following?" Quite simple: The std::enable_if with only one parameter ends in a void. And you simply can't assign a value to void. You can change your code to std::enable_if_t<!std::is_arithmetic_v<T>, bool> p = true as now your std::enable_if results in a bool where you can assign a value like true.

And with C 20 you can simplify a lot by using concepts

template <typename T> concept arithmetic = std::is_arithmetic_v<T>;

template< arithmetic T>
void foo(T) {
    std::cout << "arithmetic version called\n";
}

void foo(auto) {
    std::cout << "non arithmetic version called\n";
}

int main() {
    foo(33);
    foo("hello");
}

As you can see, we do not need to define a "non-arithmetic" concept, as we simply can trust on the rule, that the more specified template will be used, if more templates are able to be instantiated.

CodePudding user response:

Note you didn't specify different return values, both functions return a void. So there is no problem there. SFINAE will just ensure one of the functions will not result in a compilable return type (but will not give an error, since substition is not an error).

Don't get to focused on SFINAE, c keeps improving meta template programming techniques and in this case I would just use an if constexpr solution. Like this:

#include <type_traits>
#include <iostream>

template<typename T>
void foo(const T& v)  // <== I prefer not to use pass by value for template parameters so I pass by reference (avoid copying)
{
    if constexpr (std::is_arithmetic_v<T>)
    {
        std::cout << "arithmetic version called\n";
    }
    else
    {
        std::cout << "non arithmetic version called\n";
    }
}

int main() 
{
    foo(33);
    foo("hello");

    return 0; 
}
  • Related