c++c++17language-lawyersfinaereturn-type-deduction

why return type deduction can not support SFINAE with std::is_invocable_v


there is some functors with return type deduction, including lambda expressions.

constexpr auto a = [](auto&& x){ return x + 1; };
struct X{
    template<typename T>
    auto operator()(T&& x){
        return x + 1;
    }
};

and then, I have 2 functions to check if the arguments can be applied to these functors, by std::is_invocable_v and SFINAE.

template<typename F, typename T, std::enable_if_t<std::is_invocable_v<F, T>, int> = 0>
void foo(T a){
    std::cout << "yes" << std::endl;
}
template<typename F, typename T, std::enable_if_t<!std::is_invocable_v<F, T>, int> = 0>
void foo(T a){
    std::cout << "no" << std::endl;
}

finally, I use foo<X>(0) or foo<decltype(a)>(0), and it works well and says "yes" because the check is passed. but when I use foo<X>((void*)0) or foo<decltype(a)>((void*)0), I receive compile error instead of "no".

Substitution occurs in

1. all types used in the function type (which includes return type and the types of all parameters)

...

it looks like, these functors will accept arguments of any type, and then throw an error if x + 1 is ill-formed. but, the return type of operator() is deduced by x + 1, which means it depends on the argument's type T. when the std::is_invocable_v is instantiated, T is substituted by void*, and then the signature of operator() has an invalid return type. is this substitute failure?


to clarify this question, I define these 2 functors:

struct Y{
    template<typename T>
    decltype(auto) operator()(T&& x){
        return x + 1;
    }
};
struct Z{
    template<typename T>
    auto operator()(T&& x)->decltype(x + 1){
        return x + 1;
    }
};

If the return type is decltype(auto), the return type is as what would be obtained if the expression used in the return statement were wrapped in decltype.

but why foo<Z>((void*)0) says "no" but foo<Y>((void*)0) result in an error?


Solution

  • This is [dcl.spec.auto]/11.

    Deduced return types are not SFINAE-friendly, in the sense that querying the return type of a templated callable (function template / generic lambda / functor with templated operator()(...)) which leverages return type deduction requires instantiation of the particular specialization of the callable, as the definition of the specialization is needed in order to deduce the return type:

    [dcl.spec.auto]/11 Return type deduction for a function template with a placeholder in its declared type occurs when the definition is instantiated even if the function body contains a return statement with a non-type-dependent operand. [ Note: Therefore, any use of a specialization of the function template will cause an implicit instantiation. Any errors that arise from this instantiation are not in the immediate context of the function type and can result in the program being ill-formed ([temp.deduct]).  — end note ] [ Example:

    template <class T> auto f(T t) { return t; }    // return type deduced at instantiation time
    typedef decltype(f(1)) fint_t;                  // instantiates f<int> to deduce return type
    template<class T> auto f(T* t) { return *t; }
    void g() { int (*p)(int*) = &f; }               // instantiates both fs to determine return types,
                                                    // chooses second
    

     — end example ]

    Due to the implementation of std::is_invocable, which applies decltype on the (unevaluated) expression of invoking of the callable specialization (to find the return type), return type deduction is triggered for the specialization, which requires the specialization to be instantiated, which results in, in this case, as highlighted in the (non-normative) note above, the program being ill-formed.