Why it happens
The answer by max66 basically explains what's going on. But it can be a bit surprising that:
You can implicitly convert from std::shared_ptr<int>
to std::shared_ptr<void>
and not the reverse.
You can implicitly convert from std::function<void(std::shared_ptr<void>)>
to std::function<void(std::shared_ptr<int>)>
and not the reverse.
You can implicitly convert from a lambda with argument type std::shared_ptr<void>
to std::function<void(std::shared_ptr<int>)>
.
You cannot implicitly convert from a lambda with argument type std::shared_ptr<int>
to std::function<void(std::shared_ptr<void>)>
.
The reason is that when comparing whether function interfaces are more general or more specific, the rule is that return types must be "covariant", but argument types must be "contravariant" (Wikipedia; see also this SO Q&A). That is,
Given the (pseudo-code) function interface types
C func1(A1, A2, ..., An)
D func2(B1, B2, ..., Bn)
then any function which is an instance of the func2
type is also an instance of the func1
type if D
can convert to C
and every Ai
can convert to the corresponding Bi
.
To see why this is the case, consider what happens if we allow the function
-to-function
conversions for std::function<std::shared_ptr<T>>
types and then try to call them.
If we convert a std::function<void(std::shared_ptr<void>)> a;
to std::function<void(std::shared_ptr<int>)> b;
, then b
acts like a wrapper containing a copy of a
and forwarding calls to it. Then b
might be called with any std::shared_ptr<int> pi;
. Can it pass it to the copy of a
? Sure, because it can convert std::shared_ptr<int>
to std::shared_ptr<void>
.
If we convert a std::function<void(std::shared_ptr<int>)> c;
to std::function<void(std::shared_ptr<void>)> d;
, then d
acts like a wrapper containing a copy of c
and forwarding calls to it. Then d
might be called with any std::shared_ptr<void> pv;
. Can it pass it to the copy of c
? Not safely! There is no conversion from std::shared_ptr<void>
to std::shared_ptr<int>
, and even if we imagine d
somehow trying to use std::static_pointer_cast
or similar, pv
might not point at an int
at all.
The actual Standard rule, since C++17 ([func.wrap.func.con]/7) is that for the std::function<R(ArgTypes...)>
constructor template
template<class F> function(F f);
Remarks: This constructor template shall not participate in overload resolution unless f
is Lvalue-callable for argument types ArgTypes...
and return type R
.
where "Lvalue-callable" essentially means that a function call expression with perfectly-forwarded arguments of the given types is valid, and if R
is not cv void
, the expression can implicitly convert to R
, plus considerations for cases when f
is a pointer to member and/or some argument types are std::reference_wrapper<X>
.
This definition essentially automatically checks for contravariant argument types when attempting a conversion from any callable type to a std::function
, since it checks whether the argument types of the destination function
type are valid arguments to the source callable type (allowing for permitted implicit conversions).
(Before C++17, the std::function::function(F)
template constructor did not have any SFINAE-style restrictions at all. This was bad news for overloading situations like this and for templates that attempted to check whether a conversion was valid.)
Note that contravariance of argument types actually shows up in at least one other situation in the C++ language (even though it's not a permitted virtual function override). A pointer to member value can be thought of as a function which takes a class object as input, and returns the member lvalue as output. (And initializing or assigning a std::function
from a pointer to member will interpret the meaning in exactly that way.) And given that class B
is a public unambiguous base of class D
, we have that a D*
can implicitly convert to a B*
but not vice-versa, and a MemberType B::*
can convert to a MemberType D::*
but not vice-versa.
What to do
The tag dispatching max66 suggests is one solution.
Or for an SFINAE way,
void F(std::function<void(std::shared_ptr<void>)>);
void F(std::function<void(std::shared_ptr<int>)>);
// For a type that converts to function<void(shared_ptr<void>)>,
// call that overload, even though it likely also converts to
// function<void(shared_ptr<int>)>:
template <typename T>
std::enable_if_t<
std::is_convertible_v<T&&, std::function<void(std::shared_ptr<void>)>> &&
!std::is_same_v<std::decay_t<T>, std::function<void(std::shared_ptr<void>)>>>
F(T&& func)
{
F(std::function<void(std::shared_ptr<void>)>(std::forward<T>(func)));
}