2

Following up my question c++ - How do implicit conversions work when using an intermediate type?, from which I understood the rule of 1 implicit conversion max, I'm trying to understand a more advanced use case involving function arguments.

Let's say I have a function, which accepts as a parameter another function. That function parameter could either return something, or return nothing (void). Therefore, I want to overload the function definition, to accept in one case the non-void function argument, and in the other case the void function argument.

using VoidType = void (string);
using NonVoidType = string (string);

string call(VoidType *fn, string arg) {
    cout << "[using void function]" << endl;
    fn(arg);
    return arg;
}

string call(NonVoidType *fn, string arg) {
    cout << "[using non void function]" << endl;
    return fn(arg);
}

When calling the function, the given argument has a known type, so the overload selection should be straightforward, such as here:

void printVoid(string message) {
    cout << message << endl;
}

string printNonVoid(string message) {
    cout << message << endl;
    return message;
}

void test() {
    call(printVoid, "call(printVoid)");
    call(printNonVoid, "call(printNonVoid)");
}

But it's not when I have intermediate types, such as std::function wrappers:

string call(function<VoidType> fn, string arg) {
    cout << "[using void function]" << endl;
    fn(arg);
    return arg;
}

string call(function<NonVoidType> fn, string arg) {
    cout << "[using non void function]" << endl;
    return fn(arg);
}

void test() {
    call(printVoid, "call(printVoid)");
    call(printNonVoid, "call(printNonVoid)"); // more than one instance of overloaded function "call" matches the argument list:C/C++(308)
}

for which I get the error more than one instance of overloaded function "call" matches the argument list:C/C++(308) on the second call.

A solution to this is to instantiate the std::function explicitly before calling:

void test() {
    call(function(printNonVoid), "call(function(printNonVoid))");
}

But, how is call(function(printNonVoid), "xxx") so different from call(printNonVoid, "xxx") knowing that in the latter case I would expect the implicit conversion to do the equivalent to what is done in the former case.

Am I missing something, like some intermediate, hidden copy constructors or whatever?

Full example:

#include <iostream>

using namespace std;

using VoidType = void (string);
using NonVoidType = string (string);

string callFromPointer(VoidType *fn, string arg) {
    cout << "[callFromPointer] void" << endl;
    fn(arg);
    return "<void>";
}

string callFromPointer(NonVoidType *fn, string arg) {
    cout << "[callFromPointer] non void" << endl;
    return fn(arg);
}

string callFromFunction(function<VoidType> fn, string arg) {
    cout << "[callFromFunction] void" << endl;
    fn(arg);
    return "<void>";
}

string callFromFunction(function<NonVoidType> fn, string arg) {
    cout << "[callFromFunction] non void" << endl;
    return fn(arg);
}

void printVoid(string message) { cout << "\t[printVoid] " << message << endl; }
string printNonVoid(string message) {
    cout << "\t[printNonVoid] " << message << endl;
    return message;
}

void sep() { cout << "\n----\n" << endl; }

void main() {
    callFromPointer(printVoid, "callFromPointer(printVoid)");
    callFromPointer(printNonVoid, "callFromPointer(printNonVoid)");

    sep();

    callFromFunction(printVoid, "callFromFunction(printVoid)");
    callFromFunction(printNonVoid, "callFromFunction(printNonVoid)"); // more than one instance of overloaded function "callFromFunction" matches the argument list:C/C++(308)
    
    sep();

    callFromFunction(function(printVoid), "callFromFunction(function(printVoid))");
    callFromFunction(function(printNonVoid), "callFromFunction(function(printNonVoid))");
}
Yannick Meine
  • 499
  • 5
  • 11
  • 1
    What error message did you get? – Mooing Duck Sep 27 '22 at 20:49
  • Sorry, I forgot to highlight it better (it's only as a comment in one of the code snippets right now), I'll edit the question. Error is: more than one instance of overloaded function "call" matches the argument list:C/C++(308) – Yannick Meine Sep 27 '22 at 21:17

1 Answers1

8

This is because std::string(*)(std::string) is "compatible" with both std::function<void(std::string)> and std::function<std::string(std::string)> since the former can simply ignore the underlying function's return value.

That is, both of the following are valid:

std::function<void(std::string)> voidFunc(printNonVoid);
std::function<std::string(std::string)> strFunc(printNonVoid);

Since neither is a "better" match, overload resolution fails when calling callFromFunction(printNonVoid, "...").


The reason it works when using std::function(printNonVoid) is because class template argument deduction takes over, and there is a deduction guide for T(*)(Args...) -> std::function<T(Args...)>. That deduction guide turns std::function(printNonVoid) into std::function<std::string(std::string)>(printNonVoid). Since that's an exact match with no conversion necessary for the std::function<std::string(std::string)> overload of callFromFunction that overload is chose by overload resolution.

That deduction guide doesn't come into play when performing overload resolution with just the raw function name though, since the function arguments have to explicitly declare std::function's template argument.

Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
  • That's a very clear answer! I deduced the "compatibility" thing for the same logical reason, which played part in the ambiguity issue. But I was unable to understand the resolution of the ambiguity through what you called "class template deduction". I would have expected the compiler to still flag one conversion as the "better" match, but I kinda understand it prefers to "fail" and force explicitness. **It's funny how the implicit conversions are eventually different from the explicit conversions.** – Yannick Meine Sep 27 '22 at 21:25
  • So, if I want to accept both the void and non-void variants through `std::function` parameters, is my only solution to explicitly convert the arguments by wrapping them with the `function` constructor? It's overhead but if there's no alternative, I can live with it. – Yannick Meine Sep 27 '22 at 21:27
  • 1
    @YannickMeine You could make your function a template and make the callable type a template argument. Then you could do some SFINAE or concepts magic to get the compiler to prefer one overload when the callable returns a string and another when it returns nothing. Something like [this](http://coliru.stacked-crooked.com/a/cfa3c4c93ac17312). – Miles Budnek Sep 27 '22 at 22:08
  • 1
    Or [this](http://coliru.stacked-crooked.com/a/129f0caad0f39d25) if C++20 concepts are on the table. – Miles Budnek Sep 27 '22 at 22:15
  • C++ is definitely fascinating. So I hoped I wouldn't hit the point where I would need TMP, though I can see *concepts* are cleaner and respect DRY. I will try both of your examples, also checking if my whole toolchain can deal with *concepts* (it's mainly compiler and code editor support), but I'm already compiling with *latest* C++ version. However, my goal is to eventually keep the C++ part of my project as tight and simple as possible, so I may go with a different function name to avoid the overloading ambiguity issue. It's all a matter of tradeoffs. – Yannick Meine Sep 27 '22 at 22:24
  • 1
    You can make the C++17 version [more DRY](http://coliru.stacked-crooked.com/a/04f7c98e7955e9a3); I was just lazy. – Miles Budnek Sep 27 '22 at 23:06
  • 1
    The basic problem is that overload resolution only "knows" 4 "levels" of implicit conversions -- 'trivial' conversions, 'promotions', 'standard' conversions, and 'user-defined' conversions. Any conversion involving a constructor is considered 'user-defined', so all are at the same level and one will not be preferred to another. The langauge does not provide any way to say "this constructor is better that that one for argument matching/overload resolution" – Chris Dodd Sep 28 '22 at 00:00
  • @ChrisDodd So, which of the four categories does this fall into: `std::function(printNonVoid)`? In fact, here's what I thought the compiler would have done: input argument is of type `string (string)`, target parameter of types either `function` or `function `, so *implicitly convert* the `string (string)` argument to `function(myArg)`. That works if I explicitly convert on the calling end, because then I have a complete type explicitly matching one overload. But otherwise it does not work. – Yannick Meine Sep 28 '22 at 10:43
  • @ChrisDodd I realize while writing my previous comment that I may have been wrongly considering the types equal. We have 2 compatible but distinct type instantiations. The overloads accept either a `function`, or a `function `. Then when the compiler sees my call, it detects an implicit conversion must occur. It tries all the overloads to see which type can be constructed from my input, and both match thanks to the "void is compatible with non-void if we discard the return value". It's just hard for me to imagine the proper processing sequence of the compiler... – Yannick Meine Sep 28 '22 at 10:48
  • @MilesBudnek I tried with the concepts, it worked like a charm for the exposed use case and I eventually love it. However, my real life use case involves more overloading, and this time with the input parameter changing as well. So if I translate it to the example here, we would have all four: `string call(string)`, `void call(string)`, `string call(int)`, `void call(int)`. With that added, the compiler can't make the distinction between the string and int parameters anymore and call ambiguity is back. – Yannick Meine Sep 28 '22 at 10:49
  • It [should work fine](http://coliru.stacked-crooked.com/a/a3a79335e38992cf) in that case. Remember overload sets aren't first-class citizens in C++, so if, i.e. `printVoidStr` and `printVoidInt` are actually `printVoid(string)` and `printVoid(int)` then you'll have to disambiguate with a cast or wrap them in a template lambda or something. `printVoid` isn't a "thing" you can pass around in that case. – Miles Budnek Sep 28 '22 at 17:46
  • Sorry I realized the example I crafted for this question was eventually biased. [Here is one](https://coliru.stacked-crooked.com/a/8294a5cc2fb395ab) which is much closer to my real life use case: registering functions after adapting input/output. You can see that when it works, the `call` overload is "wrongly" matching the `void(int)` and `string(int)` functions. If I try adding 2 overloads for those, then it fails compiling because of ambiguity. I guess the concept is somehow "discarding" the parameter type when checking if template specialization would match, since it checks just the return. – Yannick Meine Sep 29 '22 at 10:18
  • 1
    Ah, that's my error. My `ReturnsNonVoid` concept didn't care if the callable was invokable at all with the provided argument types. [Here](https://coliru.stacked-crooked.com/a/e6f9abbd133e6289) is a version that adds that check. – Miles Budnek Sep 29 '22 at 16:59
  • Sorry for the late reply, I tried and it worked. Thanks for all the answers and tips and making me dig into TMP and concepts. I tweaked one of the predicates to use `is_convertible_v` instead of `is_same_v`, that way I can still benefit from implicit conversions of the return value. After refactoring my code though I hit a pretty nasty circular dependency, but I just learned that templated code can also be split into declarations and implementations (just that in that case implementation still must be included), so I'm following [this](https://stackoverflow.com/a/45403262/1590196). – Yannick Meine Oct 03 '22 at 18:35