Search code examples
c++c++17template-meta-programmingsfinaeboost-hana

What is the usecase of calling hana::is_valid with a nullary function?


Boost.Hana offers boost::hana::is_valid to check whether a SFINAE-friendly expression is valid.

You can use it like this

    struct Person { std::string name; };
    auto has_name = hana::is_valid([](auto&& p) -> decltype((void)p.name) { });
    Person joe{"Joe"};
    static_assert(has_name(joe), "");
    static_assert(!has_name(1), "");

However, there's a note about the argument to is_valid being a nullary function:

To check whether calling a nullary function f is valid, one should use the is_valid(f)() syntax. […]

How can I even use it by passing to it a nullary function? I mean, if a function is nullary, then how is its body gonna have any dependent context to which SFINAE can apply?

I think that maybe "lambda captures" might have something to do with the answer, but I can't really figure it out how.


Solution

  • Use case is that of checking that f is actually nullary, e.g

    if constexpr (is_valid(f)()) {
      f(); // Treat f as a "no args function"
    } else if constexpr (is_valid(f, arg1)) {
      f(arg1);
    }
    

    what the documentation says is that unlike functions of non zero arity, the is_valid predicate can only be invoked in the form:

    is_valid(f)(); // Invoking "is_valid(f)", i.e. no parentheses, does NOT
                   // check that f is a nullary function. 
    

    reminder: to check whether e.g. a 2 arguments function call is valid you can say:

    is_valid(f, arg1, arg2);
    // or 
    is_valid(f)(arg1, arg2)
    

    Take for example the following Demo

    void f0()
    {
        std::cout << "Ok f0\n";
    }
    
    void f1(int)
    {
        std::cout << "Ok f1\n";
    }
    
    template <class F>
    void test(F fun)
    {
        if constexpr (hana::is_valid(fun)())
        {
            fun();
        }
        else if constexpr (hana::is_valid(fun, 2))
        {
            fun(2);
        }
    }
    
    int main() {
    
        test(f0);
        test(f1);
    }
    

    It may be obvious, but you have to keep in mind that SFINAE does not happen ON f0 or f1. SFINAE happens in the guts of is_valid between the different flavors of is_valid_impl (comments mine):

    // 1
    template <
        typename F, typename ...Args, typename = decltype(
        std::declval<F&&>()(std::declval<Args&&>()...)
    )>constexpr auto is_valid_impl(int) { return hana::true_c; }
    
    // 2
    template <typename F, typename ...Args>
    constexpr auto is_valid_impl(...) { return hana::false_c; }
    
    // Substitution error on 1 will trigger version 2
    

    So your question

    "I mean, if a function is nullary, then how is its body gonna have any dependent context to which SFINAE can apply?"

    has little meaning since SFINAE does not happen on the user provided function. After all, we are setting up nothing to enable SFINAE on our functions. Implementing SFINAE requires to provide more than 1 candidates that "guide" the instantiation process (check SFINAE sono buoni).

    The term "SFINAE friendly" here, has to do with f (our function) being usable as type parameter for the SFINAE implementation in is_valid. For example, if in our Demo f was overloaded (replace f1(int) with f0(int)) you would get a substitution error:

    <source>:22:6: note: candidate: 'template<class F> void test(F)'
       22 | void test(F fun)
          |      ^~~~
    <source>:22:6: note:   template argument deduction/substitution failed:
    

    because when the compiler reaches the deepest point of is_valid_impl it tries to instantiate the favorable version (version 1, that doesn't have ... parameter) but it cannot tell what the type of F is and produces a hard error.

    For reference, the way SFINAE works is that if it could use type F, the compiler would:

    1. make an attempt on version 1
    2. If successfull return true (so IT IS valid)
    3. If not successfull it would go for version 2 (substitution error is not a failure) which does not use type F as F(Args...) and hence produce a false.