12

This is a follow up of this question: Why can I call a callable that is const-referenced and where the actual callable is a mutable lambda?

I can mimic the implicit conversion of a const mutable closure to function pointer when it's called like this:

#include <iostream>

void dummy() { std::cout << "call dummy\n";}

using fptr = void(*)();

struct callable {
    void operator()() {std::cout << "call callable\n"; }
    operator fptr() const { 
        std::cout << "convert\n";
        return &dummy; 
    }
};

int main()
{
    auto exec = []( auto const &fn ) { 
        std::cout << "call\n";
        fn(); 
    }; 
    exec( callable{} );
}

Live Demo

I do not understand why the implicit conversion is triggered. It does not take place when passing the argument (the argument is deduced, hence does not take into account implicit conversion) but only when () is invoked as evident from the output:

call
convert
call dummy

I tried to experiment to understand what is going on. Trying something similar with a different operator does not work (as expected):

#include <iostream>

struct bar {
    void operator+(int) const { std::cout << "bar\n"; };   
};

struct foo {
    void operator+(int) { std::cout << "foo\n"; }
    operator bar() const { return  {}; }
};

int main() {
    auto exec = []( auto const &fn ) { 
        std::cout << "call\n";
        fn + 42; 
    }; 
    exec(foo{});
};

error:

<source>:15:12: error: passing 'const foo' as 'this' argument discards qualifiers [-fpermissive]
   15 |         fn + 42;
      |         ~~~^~~~

What is special about the call operator? How is it possible that fn() implicitly converts fn to call something else?


PS: Meanwhile I discovered that it must be related to conversion to function pointer, because when I turn the first example to something more similar than the second I get the expected error:

#include <iostream>

struct foo {
    void operator()() const { std::cout << "call foo\n"; }

};

struct callable {
    void operator()() {std::cout << "call callable\n"; }
    operator foo() const { return {}; };
};

int main()
{
    auto exec = []( auto const &fn ) { 
        std::cout << "call\n";
        fn(); 
    }; 
    exec( callable{} );
}

Now I get the expected error

<source>:17:11: error: no match for call to '(const callable) ()'
   17 |         fn();
      |         ~~^~

Now the question is: What is special about conversions to function pointer type?

It appears that fn() takes into account conversions to function pointer. Why?

4
  • 1
    This is a difference between fundamental types and user-defined types. If you add a conversion to int then it will be implicitly used in the + case as well. Commented Apr 25 at 19:17
  • @MilesBudnek can you turn that into an answer? After the first surprise I can accept that it is like this, but I wouldnt be able to find any reference that explains it Commented Apr 25 at 19:18
  • 1
    I'm looking for a standard reference. I'll write an answer if I can find one. Commented Apr 25 at 19:19
  • I was wrong, this is just special behavior specified for conversions to function types (or pointers or references to them). I assume the + case also has some special case defined for conversions to built-in integral types, but I'm not going to go digging for that one right now. Commented Apr 25 at 19:53

2 Answers 2

10

This special behavior for conversion to function types is defined in [over.call.object]:

If the postfix-expression E in the function call syntax evaluates to a class object of type "cv T", then the set of candidate functions includes at least the function call operators of T. The function call operators of T are the results of a search for the name operator() in the scope of T.

In addition, for each non-explicit conversion function declared in T of the form

operator conversion-type-id ( ) cv-qualifier-seq ref-qualifier noexcept-specifier attribute-specifier-seq ;

where the optional cv-qualifier-seq is the same cv-qualification as, or a greater cv-qualification than, cv, and where conversion-type-id denotes the type “pointer to function of (P1,…,Pn) returning R”, or the type “reference to pointer to function of (P1,…,Pn) returning R”, or the type “reference to function of (P1,…,Pn) returning R”, a surrogate call function with the unique name call-function and having the form

R call-function ( conversion-type-id  F, P1 a1, …, Pn an) { return F (a1, …, an); }

is also considered as a candidate function. Similarly, surrogate call functions are added to the set of candidate functions for each non-explicit conversion function declared in a base class of T provided the function is not hidden within T by another intervening declaration.

The argument list submitted to overload resolution consists of the argument expressions present in the function call syntax preceded by the implied object argument (E).

In your example case, the postfix-expression E is fn, which evaluates to a class object of type const callable. Since callable has a conversion operator to "pointer to function of () returning void" and that operator is const-qualified, then the surrogate function void unique_name(fptr F) { return F(); } is imagined, and the call unique_name((fn)) is considered for the expression fn().

Sign up to request clarification or add additional context in comments.

Comments

6

To really make the + case equivalent, it'd be something like this:

struct bar {};
void operator+(bar const&, int) { std::cout << "bar\n"; };  

And then indeed it compiles and outputs:

call
bar

The difference is that member bar::operator+(int) is not a candidate, while non-member ::operator+(bar const&, int) is (since it's in scope; it won't be found via ADL if you wrap it in a namespace - conversion operator return types do not contribute to ADL in this way).

Integers do actually have such built-in non-member operators defined, as described in [over.built/10]. The behavior for the call expression is defined separately in [over.call.object] but it's essentially the same as if non-member ::operator()(F*) were a thing and defined for every function type F.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.