How to do function overloading with std::shared_ptr<void> and another type of std::shared_ptr?

I'm confused but I try an explanation.

I see that your lambda can be accepted by both std::function<void(std::shared_ptr<void>)> and std::function<void(std::shared_ptr<int>)>; you can verify that both the following lines compile

std::function<void(std::shared_ptr<void>)>  f0 = [](std::shared_ptr<void>){};
std::function<void(std::shared_ptr<int>)>   f1 = [](std::shared_ptr<void>){};

And this is because (I suppose) a shared pointer to int can be converted to shared pointer to void; you can verify that the following line compile

std::shared_ptr<void> sv = std::shared_ptr<int>{};

At this point we can see that calling

c.F([](std::shared_ptr<void>) {});

you don't pass a std::function<void(std::shared_ptr<void>)> to F(); you're passing an object that can be converted to both std::function<void(std::shared_ptr<void>)> and std::function<void(std::shared_ptr<int>)>; so an object that can be used to call both versions of F().

So the ambiguity.

Is there any way to workaround this ambiguity? Perhaps with SFINAE?

Maybe with tag dispatching.

You can add an unused argument and a template F()

void F (std::function<void(std::shared_ptr<void>)>, int)
 { std::cout << "void version" << std::endl; }

void F (std::function<void(std::shared_ptr<int>)>, long)
 { std::cout << "int version" << std::endl; }

template <typename T>
void F (T && t)
 { F(std::forward<T>(t), 0); }

This way calling

c.F([](std::shared_ptr<void>) {});
c.F([](std::shared_ptr<int>){});

you obtain "void version" from the first call (both non-template F() matches but the "void version" is preferred because 0 is a int) and "int version" from the second call (only the F() "int version" matches).


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)));
}