7

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?

Boann
  • 48,794
  • 16
  • 117
  • 146
iolo
  • 1,090
  • 1
  • 9
  • 20
  • I think it would help me (and the OP of course) if the answers could explain why `static_assert( std::is_invocable_v == false );` is fine but `static_assert( std::is_invocable_v == false );` is an error. That information seems to be missing – Kevin Mar 20 '20 at 14:07
  • @Kevin - the difference is that `Foo::operator()()` doesn't exist where `ForwardToFoo::operator()()` exist (well... the compiler can generate it with `Args...` empty) but **inside** its body gives an error. – max66 Mar 20 '20 at 14:17
  • @max66 Your edit to your question explains that and the fix nicely, thanks – Kevin Mar 20 '20 at 14:18
  • 1
    See also [What does it mean when one says something is SFINAE-friendly?](https://stackoverflow.com/q/35033306/2069064) – Barry Mar 20 '20 at 14:19
  • @Kevin - exactly: after the fix `ForwardToFoo::operator()()` doesn't exist anymore. – max66 Mar 20 '20 at 14:20

3 Answers3

6

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.

max66
  • 65,235
  • 10
  • 71
  • 111
  • This is what I was looking for! Thanks! – iolo Mar 20 '20 at 14:17
  • 5
    I didn't see anyone mention, but it is relevant, and in a way the forwarding does come into play: is_invocable doesn't itself cause instantiation; it only uses the declaration. But the function uses a decltype(auto) return type so it is *that* which triggers instantiation. If the return type were known (say, int), the body would not be instantiated, and is_invocable would return true. (is_invocable does not answer can you call it without error. it answers whether the declaration says you can call it.) – Jeff Garrett Mar 20 '20 at 14:57
  • @JeffGarrett - Good point. I've missed it. Added in the answer. Thanks. – max66 Mar 20 '20 at 17:51
  • @iolo - answer improved following an important observation from Jeff Garrett. – max66 Mar 20 '20 at 17:51
  • That is really useful! Thanks! – iolo Apr 22 '20 at 08:31
1

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?

Asteroids With Wings
  • 17,071
  • 2
  • 21
  • 35
0

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

florestan
  • 4,405
  • 2
  • 14
  • 28