Search code examples
c++c++17perfect-forwarding

Why can std::is_invocable not handle forwarding?


I have a class that is simply forwarding the function call to another class and I would like to be able to use std::invocable<> on my forwarding class. But for some reason that fails... Is this what I should expect? Is there a way to work around it?

#include <type_traits>
#include <utility>

struct Foo {
    constexpr int operator()( int i ) const {
        return i;
    }
};

struct ForwardToFoo {
    template<class ...Args>
    constexpr decltype(auto) operator()( Args &&...args ) const {
        Foo foo;
        return foo( std::forward<Args>( args )... );
    }
};

int main( void ) {
    // These work fine
    static_assert( std::is_invocable_v<ForwardToFoo, int> == true );
    static_assert( std::is_invocable_v<Foo, int> == true );
    static_assert( std::is_invocable_v<Foo> == false );

    // This causes a compile error
    static_assert( std::is_invocable_v<ForwardToFoo> == false );

    return 0;
}

Edit: The answers so far suggest that the issue is that the last static_assert() forces ForwardToFoo::operator()<> to be instantiated without arguments hence triggering a compile error. So is there a way to turn this instantiation error into a SFINAE error that can be handled without a compile error?


Solution

  • You get the same error that you get from

    ForwardToFoo{}();
    

    you have that the operator() in ForwardToFoo is invocable without arguments. But when it call the operator in Foo(), without arguments... you get the error.

    Is there a way to work around it?

    Yes: you can SFINAE enable ForwardToFoo()::operator() only when Foo()::operator() is callable with the arguments.

    I mean... you can write ForwardToFoo()::operator() as follows

    template<class ...Args>
    constexpr auto operator()( Args &&...args ) const
       -> decltype( std::declval<Foo>()(std::forward<Args>(args)...) ) 
     { return Foo{}( std::forward<Args>( args )... ); }
    

    -- EDIT --

    Jeff Garret notes an important point that I missed.

    Generally speaking, the simple use of std::invokable doesn't cause the instantiation of the callable in first argument.

    But in this particular case the return type of ForwardToFoo::operator() is decltype(auto). This force the compiler to detect the returned type and this bring to the instantiation and the error.

    Counterexample: if you write the operator as a void function that call Foo{}(), forwarding the arguments but not returning the value,

    template <typename ... Args>
    constexpr void operator() ( Args && ... args ) const
     { Foo{}( std::forward<Args>( args )... ); }
    

    now the compiler know that the returned type is void without instantiating it.

    You also get a compilation error from

    static_assert( std::is_invocable_v<ForwardToFoo> == false );
    

    but this time is because ForwardToFoo{}() result invocable without arguments.

    If you write

    static_assert( std::is_invocable_v<ForwardToFoo> == true );
    

    the error disappear.

    Remain true that

    ForwardToFoo{}();
    

    gives a compilation error because this instantiate the operator.