13

Try out this following code:

#include <functional>
#include <memory>

class C {
    public:
    void F(std::function<void(std::shared_ptr<void>)>){}
    void F(std::function<void(std::shared_ptr<int>)>){}
};

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

You'll see a compile error:

prog.cc:12:7: error: call to member function 'F' is ambiguous
    c.F([](std::shared_ptr<void>) {});
    ~~^
prog.cc:6:10: note: candidate function
    void F(std::function<void(std::shared_ptr<void>)>){}
         ^
prog.cc:7:10: note: candidate function
    void F(std::function<void(std::shared_ptr<int>)>){}
         ^

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

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
Ale Morales
  • 2,728
  • 4
  • 29
  • 42
  • And here's a link to the code in case you just want to click a button: https://wandbox.org/permlink/YMa7UaBYaIHaWPE2 – Ale Morales Feb 05 '19 at 21:08
  • Marginally related: Can you do a `shared_pointer` to `void`? At the very least you'll need a custom deleter. Never done this because there aren't many uses for `void` pointers in C++ In C all the time, but no smart pointers in C. – user4581301 Feb 05 '19 at 21:12
  • 4
    workaround is rather simple, you could for example, explicitly convert to `std::fucntion` with the right signature, imho its more interesting why there is ambiguity – 463035818_is_not_an_ai Feb 05 '19 at 21:13
  • @user4581301 It's not common but it's totally valid, and yeah you have to be careful. Sometimes you don't need the custom deleter btw, but maybe that's because clang is "smart enough" to fix that, idk, never tried in a different platform. – Ale Morales Feb 05 '19 at 21:15
  • 1
    anyhow, also interesting how four lines of code can turn a "no idea what is your actual problem nor what you are asking about" question into something really interesting, sorry couldnt help myself :P – 463035818_is_not_an_ai Feb 05 '19 at 21:15
  • Yeah, right now I'm explicitly declaring the type but it's a pain in the a.., I would like the compiler to automatically get the type so I can call these functions like in the example. I guess it has to do with shared_ptr being some kind of catch-all shared_ptr. – Ale Morales Feb 05 '19 at 21:17
  • 1
    I almost posted an answer but the code is behaving opposite of what I'd expect. If you use `c.F([](std::shared_ptr) {});` it compiles fine, when I expect it would be an ambiguity. `c.F([](std::shared_ptr) {});` shouldn't be ambiguous since you can't go from `std::shared_ptr` to `std::shared_ptr`, only the opposite is allowed. – NathanOliver Feb 05 '19 at 21:20
  • @NathanOliver Noticed that as well, pretty weird ... – Ale Morales Feb 05 '19 at 21:21
  • my guess it that is is [overload 9](https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr) and implicit conversion from `int*` to `void*` but none from `void*` to `int*` – 463035818_is_not_an_ai Feb 05 '19 at 21:24
  • 1
    @user463035818 But how does that explain the ambiguity? The OP is calling the function with a functor of `void(std::shared_ptr)`, so it shouldn't convert to a `void(std::shared_ptr)` – NathanOliver Feb 05 '19 at 21:27
  • @NathanOliver oh right I got lost in the details and thought it was the other way around... now I am even more puzzled – 463035818_is_not_an_ai Feb 05 '19 at 21:28
  • @NathanOliver If you're still interested in why these `std::function` conversions are not intuitive, my answer tries to explain. – aschepler Feb 06 '19 at 05:19
  • 1
    @user463035818 If you're still interested in why these `std::function` conversions are not intuitive, my answer tries to explain. – aschepler Feb 06 '19 at 05:19
  • @almosnow I played around with smart pointers to `void`. By defauylt it winds up calling `delete` on a `void` pointer. Quoting a footnote for [**expr.delete**/1](http://eel.is/c++draft/expr.delete#1): *This implies that an object cannot be deleted using a pointer of type `void*` because `void` is not an object type.* Since I can't find a description of what will happen if you `delete` a `void*` this probably means undefined behaviour awaits. – user4581301 Feb 07 '19 at 04:45
  • @user4581301 Good to know thanks, I assumed it worked but it definitely is UB. I better declare explicit destructors anyway. – Ale Morales Feb 07 '19 at 19:37

2 Answers2

6

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).

max66
  • 65,235
  • 10
  • 71
  • 111
  • This is really good, thanks. I'm not 100% convinced by the explanation but yes you could be right, definitely. Also, I learned about tag dispatching, reading about that now, wow. – Ale Morales Feb 05 '19 at 23:36
  • Great, now it makes sense. It's allowed because the `shared_ptr` that the `std::function` expects can be passed to a functor that takes a `shared_ptr`. That was where my disconnect was. +1 – NathanOliver Feb 06 '19 at 13:32
  • @NathanOliver - exactly; but initially I was confused because I was thinking was the contrary. I find it a little counterintuitive. – max66 Feb 06 '19 at 18:15
4

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)));
}
Community
  • 1
  • 1
aschepler
  • 70,891
  • 9
  • 107
  • 161