How is conversion to function pointer type special that it enables implicit conversion of a callable?

This is a follow up of this quesiton: Read more
I can mimic the implicit conversion of a const mutable closure to function pointer when its 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{} );
}
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{});
};
<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?
Answer
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 "cvT
", then the set of candidate functions includes at least the function call operators ofT
. The function call operators ofT
are the results of a search for the nameoperator()
in the scope ofT
.In addition, for each non-explicit conversion function declared in
T
of the formoperator 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) returningR
”, or the type “reference to function of (P1,…,Pn) returningR
”, a surrogate call function with the unique name call-function and having the formR 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 withinT
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 a surrogate function void unique_name(void(*F)()) { return F(); }
is considered, with the call call unique_name((fn))
.
Enjoyed this article?
Check out more content on our blog or follow us on social media.
Browse more articles