On Thursday, 8 February 2018 at 22:57:04 UTC, Jonathan M Davis wrote:
D tends to be very picky about what it puts in overload sets in order to avoid function hijacking - e.g. it doesn't even include base class functions in an overload set once you've declared one in a derived class unless you explicitly add an alias to the base class function in the derived class.

Also, D doesn't support "best match" with function overloads. The matches have to be exact - e.g. as soon as implicit conversions come into play, there can only be one match. If there's ever a conflict, you get a compilation error (unlike in C++, which tries to figure out what it thinks the best match is and go with that, sometimes with surprising results). And having both the member functions and free functions in the same overload set would basically require the compiler to go with the "best match," which simply isn't how D deals with overloads. Sometimes, that's annoying, but it also prevents a lot of subtle bugs that tend to crop up in C++ code.

https://dlang.org/spec/function.html#function-overloading https://dlang.org/articles/hijack.html

- Jonathan M Davis

Thanks for the articles, they shed a bit more understanding.

But I have some concerns/issues that maybe you or others can help out with a bit more.

First, the link on function-overloading says "The function with the best match is selected. " So I'm not sure what you mean about subtle bugs in C++ and D doesn't support best match. If you have foo(long), food(int) and call foo(3) in the same module, the best match is selected in D and in C++. But, anyway, ignoring C++ for now, after reading those articles you linked to my main issues are D related, not C++, let me try to explain:

The rules of resolving overload sets seem to be to resolve the issue where two overload sets have a matching function and one is silently better. This is understandable and makes sense.

The case of a free function not being considered if there is a member functions seems unrelated (as you say they are not part of the overload set, but it seems like like maybe they should be). In this case the member function does not match. If overload set resolution was applied the free function would be considered and be chosen as the only available candidate.

The rules for overriding, where functions with the same name in a base class are hidden, also make sense. But not relevant here. Infact, the hijacking article makes it seem to me that ufcs hijacking has maybe been overlooked? Consider two modules which are unrelated:

module a;

string f(R)(R r, int i) {
    return "a.f";
}

string g(R)(R r) {
    return "a.g";
}

---------

module b;

auto algo() {
    static struct R {
        void popFront() {}
        enum empty = false;
        int front() { return 1; }
    }
    return R();
}

----------
// Application programmer;

import std.stdio: writeln;
import a: f, g;
import b: algo;

void main(string[] args) {
    auto r = algo();
    auto a = r.f(3);
    auto b = r.g;
    writeln(a); // will print a.f
    writeln(b); // will print a.g
}

All good so far. Until module b decides to add an internal function to it's R type:

auto algo() {
    static struct R {
        // Add a function
        string g() { return "internal g"; }
    }
    return R();
}

Application programmer updates module b...

auto b = r.g; // Does not do what programmer thinks anymore, and no warnings.

Because a.g does not participate in overload set resolution rules, the application programmer has an unknown bug (is it just me or is this a bit... to put it lightly... scary?)

So that's the first problem. Then, secondly, say the people who make module b like using proper access control levels, and mark the internal string g() as private. Now there's a compiler error in the application programmer, with no real clues as to why, unless you deep dive in to D lang specs. And then you find out you're getting a compiler error because of a private, internal, function in a Voldermort type... ouch. Like. Big ouch. But ok, this can at least be worked around:

import a: f, c = g;

Now:

auto b = r.c; // ok.

Third problem:

Module b person adds another internal function to their type.

auto algo() {
    static struct R {
        // Add a function
        string f() { return "internal f"; }
    }
    return R();
}

Now application programmer has a compile error again. Again with no clue. But this time it makes no sense because the line:

auto a = r.f(3);

Is clearly and unambiguously calling a.f(int). Again here if the free function was part of the overload set resolution rules, it would be resolved correctly.

Granted problem two and three are related. But there're subtle semantic difference there. Either this has been overlooked or there's a reason it is like this and must be like this that I've yet to hear.

As it is right now (someone correct me if I'm wrong), for someone who is writing libraries, it seems either impractical to use ufcs because of compilation errors that it would cause in client code, or, worse, unsafe because of silent bugs that would occur inside client code AND library code in the case of types adding matching function (i.e. same signature/name) in modules that the library author and application author have no knowledge, or control of.

Cheers,
- Ali

Reply via email to