0

I wrote the smallest example I could get to explain my issue, but I'll try explaining first.

So, let's say I have a method M, accepting a type T, which is a class. I can:

  • call M with a value of type T
  • call M with a value of type U, if T has a constructor T(U)

But it seems I can't call M with a value of type V, even though U(V) exists, and T(U) as well.

So, direct conversions work, but indirect ones don't. I could get it working by passing an initializer list instead: M({V}).

However, I just did that through intuition, and I have no clue as to why it works with the initializer, and why it doesn't without. To me it sounds like the compiler has the ability to perform those chained conversions then, but chooses to do so only in some arbitrary cases.

With that, you'll see in the example below a use case where passed types are function types, and that gets even more confusing to me, so there's a side question: why aren't return types used to distinguish method parameters?

In other words: as an example M(function<void (string)>) and M(function<string (string)>) are ambiguous, but only when passing a non-void function as argument. It seems that a void function argument one can only match the first signature, while a non-void function argument can match both.

How can I make the distinction between them to be able to provide a default return value for the void function but not the non-void one?

Example:

struct FunctionConverter {
    using Function = function<string (string)>;
    using FunctionVoid = function<void (string)>;

    Function fn;
    FunctionConverter(Function fn): fn(fn) {}
    FunctionConverter(FunctionVoid fn) {
        this->fn = [=](string value) {
            fn(value);
            return "default value";
        };
    }

    void operator()(string value) { fn(value); }
};

void call(string value, FunctionConverter fn) { fn(value); }

void print(string message) { cout << "[print] " << message << endl; }
string printAndReturn(string message) {
    cout << "[printAndReturn] " << message << endl;
    return message;
}

void main() {
    FunctionConverter a(print);
    FunctionConverter b(printAndReturn); // more than one instance of constructor "FunctionConverter::FunctionConverter" matches the argument list:C/C++(309)

    call("you work", {print});
    call("you don't work", print); // no suitable constructor exists to convert from "void (std::string message)" to "FunctionConverter"C/C++(415)
}

If the example structure looks a bit contrived, it's because I want it to match as close as possible my actual code, where the API design makes more sense.

Thanks in advance

Yannick Meine
  • 499
  • 5
  • 11
  • 1
    TL:DR of the dupe: You are allowed only up to one user defined conversion when implicitly done. You can have as many explicit conversions as you want. – NathanOliver Sep 26 '22 at 20:45
  • Thanks for the dupe thread, it was not easy to find for me. So, it explains the rule of one implicit conversion, which I will keep in mind. – Yannick Meine Sep 26 '22 at 21:33
  • So, I understand for `call("you don't work", print);` that `print` => `std::function` => `FunctionConverter` breaks the rule, but I don't understand how `call("you work", {print});` doesn't. I'm not familiar with initializer lists. I would imagine it's here equivalent to passing `FunctionConverter(print)`, which would use only 1 implicit conversion to build a `std::function`. Is that correct? – Yannick Meine Sep 26 '22 at 21:39
  • 1
    `{print}` in the parameter tells the compiler you want to explicitly convert `print` to the type of the function parameter. – NathanOliver Sep 26 '22 at 21:42
  • Ok, that adds up. I guess it's a very small syntax overhead I can live with to "gain" 1 level of conversion. And regarding my side question about void/non-void functions, it'll make more sense if I post another question (after doing more researches for that part, which I didn't) – Yannick Meine Sep 26 '22 at 21:54

0 Answers0