Why can std::is_invocable not handle forwarding?

Viewed 379

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?

3 Answers

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.

I can't quite work out why you expected this to work.

Foo requires an int to be callable, so ForwardToFoo does too. Otherwise its call to Foo will be ill-formed.

It doesn't really matter whether you're forwarding the arguments or copying them or anything else: they still have to be provided.

Think about how you would invoke ForwardWithFoo. Could you do it without arguments? What would happen?

The problem is not related to forwarding or not. In your last static_assert you request the compiler to instatiate the ForwardToFoo::operator() without arguments. Within that operator you call Foo::operator() which has only an overload taking an int. This is obviously causing a hard compiler error.

std::is_invocable works in the other cases as it does not actually instantiate the non-existing operators.

A potential fix to your code would be to force at least one parameter being passed to ForwardToFoo::operator():

struct ForwardToFoo {
    template<class Arg0, class ...Args>
    constexpr decltype(auto) operator()( Arg0 arg0, Args ...args ) const {
        Foo foo;
        return foo(arg0, args...);

    }
};

Then, the compiler will not instantiate that operator (because it is not callable without parameter).

Or you can use expression-sfinae as showed in the other answer.

See live example

Related