Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 520
iolo Avatar asked Mar 20 '20 13:03

iolo


Video Answer


2 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.

like image 53
max66 Avatar answered Oct 01 '22 05:10

max66


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?

like image 29
Asteroids With Wings Avatar answered Oct 01 '22 06:10

Asteroids With Wings