Dynamic overloading

Started by
17 comments, last by ToohrVyk 17 years, 6 months ago
Unrestricted overloading C++-style is problematic, hence G'Caml and typeclasses in Haskell. This might not be such a problem if your language is not type-inferred, I can't remember the details.
Advertisement
Quote:Original post by Rebooted
Unrestricted overloading C++-style is problematic, hence G'Caml and typeclasses in Haskell. This might not be such a problem if your language is not type-inferred, I can't remember the details.


I am considering a language which is not type-inferred (Java would be a good example of language where I'd apply this).
Wait, wait, wait.

Do you ask why C++ doesn't perform multiple dispatch? I think the main reason is performance - outlined by the "pay for what you use" philosophy. You example is quite simple, but if it was allowed, the language should also allow
struct S{  void frobnicate(const A& a);  void frobnicate(const B& b);};A *a = new B;S s;s.frobnicate(*a);

It would be very inconsistent if it disallowed such code and allowed yours.
Then it should also accept making those method virtuals, and this suppose some kind of introspection (although it can be limited to knowing the type of the parameter) - but it would allow double dispatch and solve the conceptual prblems that I have with the visitor pattern, which would be quite cool :)

Since such construct would be valid C++, it means that the overhead produced by the RTTI system would not be optional (as it currently is) but systematic. This would not be very consistent with the language design philosophy.

Hey, that's why C++ is an OO language, not a full blown object language [smile]

Beside that, I'm not sure that it pose much problems with namespaces. An extended form of Koenig's lookup can come to the rescue, or maybe another carefully constructed lookup rule. Anyway, defining how namespace and this interact solves the namespace problem. I'm also quite confident that multiple dispatch does not lead to conceptual computability problems - if it were, we would be in trouble with languages such as Nice (where this technique is implemented).

Did I miss something important (I felt I have [wink])
Quote:Original post by Emmanuel Deloget
It would be very inconsistent if it disallowed such code and allowed yours.
Then it should also accept making those method virtuals, and this suppose some kind of introspection (although it can be limited to knowing the type of the parameter) - but it would allow double dispatch and solve the conceptual prblems that I have with the visitor pattern, which would be quite cool :)


The compiler could implement the visitor pattern itself, since it's pretty easy to determine which functions are virtual and which aren't. This would add no overhead to constructs which already exist (my point is that almost nobody uses this "lack-of-feature" in C++) but allow more behaviour.

Quote:Since such construct would be valid C++, it means that the overhead produced by the RTTI system would not be optional (as it currently is) but systematic. This would not be very consistent with the language design philosophy.


The RTTI is not necessary, because all that information can be safely added to the vtable in a visitor-pattern approach. It would, however, add a vtable to objects without virtual functions if they ever are used polymorphically (but that should come as no surprise).

Quote:Beside that, I'm not sure that it pose much problems with namespaces. An extended form of Koenig's lookup can come to the rescue, or maybe another carefully constructed lookup rule. Anyway, defining how namespace and this interact solves the namespace problem.


Lookup here does not work, since the behaviour of a dynamically-bound function call must be the same everywhere in the program, regardless of visible namespaces. Although I agree it can be solved.

Quote:I'm also quite confident that multiple dispatch does not lead to conceptual computability problems - if it were, we would be in trouble with languages such as Nice (where this technique is implemented).


There is at least one problem, which is static verification: given the set of all possible implementations of a message, how to prove that no call will ever be ambiguous? If D is-a B, and a function f(B,B) with overloads f(D,B) and f(B,D) is called on two D's, which overload should be used? Additional constraints: handle a potentially infinite number of types, instead of only a finite lattice, by using first-class functions and parametric types.

I'm pretty sure that Nice (or CLOS, or several others, for that matter) do not perform static checking but instead either rely on runtime dispatch or resolve ambiguities arbitrarily.
Quote:Original post by ToohrVyk
To rephrase my question better: why perform overloading based on the static type of the object reference, instead of polymorphic dispatch based on the dynamic type of the object itself?


1) Because C++ is focused on static typing and verification (everything has a clear mapping at compile time - there's no possibility for throwing based on the absense of a function without implementing such functionality yourself and depending on OS specific dynamic library APIs)

2) For consistency with pointers, which are consistent with C's pointers - for obvious reasons.

Fortunately, there are various alternatives to get your desired behavior for you to pick from:

1) visitor pattern
2) chain of dynamic_cast conditionals
3) chain of type_info conditionals
4) associative map of type_info wrappers
5) member virtual function
6) using a different (more dynamic) language

I've picked #6 personally. The language I chose dosn't even *have* static type info :D.
Quote:Original post by MaulingMonkey
1) Because C++ is focused on static typing and verification (everything has a clear mapping at compile time - there's no possibility for throwing based on the absense of a function without implementing such functionality yourself and depending on OS specific dynamic library APIs)


The validity of an overload (both that the function exists, and that it is nonambiguous) can be done at linking time, in less time complexity than it takes to actually build the dispatch tables.

On the other hand, dispatch tables are quadratic in size (as consistent with their visitor-pattern implementation).

Quote:2) For consistency with pointers, which are consistent with C's pointers - for obvious reasons.


I agree. Damn pointers!

Quote:
I've picked #6 personally. The language I chose dosn't even *have* static type info :D.


I'm also in the process of building and then picking #6. Out of curiosity, which language do you use?

Quote:Original post by ToohrVyk
Quote:
I've picked #6 personally. The language I chose dosn't even *have* static type info :D.


I'm also in the process of building and then picking #6. Out of curiosity, which language do you use?


Ruby.

C++ interoperability currently sucks, unfortunately. Their C headers are macro soup in places, so you can't for example use SWIG to make a C++ class available to ruby without first #undefing twenty bazillion things like "close", "open", "write", etc. - at least, assuming you want to use a standard header like, say, <iostream>.

That said, the language just feels so damn natural compared to what I'm used to that I'm currently taking a stab at writing my own interpreter for it again. We'll see how that turns out. It will of course offer much better C++ integration should it ever be finished.

It's worth noting Ruby is a lot more advanced at interoperating with other languages than it is with C++. Funny story: I had no problems using Ruby to load Python modules written using boost::python in C++.
Quote:Original post by ToohrVyk
Quote:Original post by MaulingMonkey
1) Because C++ is focused on static typing and verification (everything has a clear mapping at compile time - there's no possibility for throwing based on the absense of a function without implementing such functionality yourself and depending on OS specific dynamic library APIs)


The validity of an overload (both that the function exists, and that it is nonambiguous) can be done at linking time, in less time complexity than it takes to actually build the dispatch tables.


Just to expand on this also: Not for delay-loaded libraries that may or may not introduce new classes and/or overloads.
The case of adding new classes is quite irrelevant, since their addition does not alter the validity of the message implementations, and can be statically verified as if the library did not exist at all (even if these classes inherit from library classes). The same is true for the creation of new messages that do not exist in the dynamic library.

The problem arises from the creation of a new implementation for a message that already exists in the library. First, the verification algorithm is incremental: this means that the message implementations in the executable can be verified as they are inserted into the library. This verification takes linear time in the number of implementations for the given message in the worst case (but most often it is done in constant-time). Additional constraints on the structure of the loaded library might eliminate the need for individual verification altogether.

This topic is closed to new replies.

Advertisement