2

C++ only supports single dynamic dispatch methods. Indeed, the following program:

#include <iostream>

struct Shape {
  virtual void overlap(Shape* y) { std::cout << "Shape, Shape\n"; }
};

struct Circle: Shape {
  void overlap(Shape* y) { std::cout << "Circle, Shape\n"; }
  void overlap(Circle* y) { std::cout << "Circle, Circle\n"; }
};

void overlap(Shape* x, Shape* y) { std::cout << "Shape, Shape\n"; }
void overlap(Circle* x, Shape* y) { std::cout << "Circle, Shape\n"; }
void overlap(Circle* x, Circle* y) { std::cout << "Circle, Circle\n"; }

int main() {
  Shape* x = new Circle();
  Shape* y = new Circle();
  x->overlap(y);
  overlap(x, y);
  return 0;
}

outputs:

Circle, Shape
Shape, Shape

If C++ supported multiple dynamic dispatch methods (i.e. with privileged access to a single parameter, the message receiver) and multiple dynamic dispatch functions (i.e. without privileged access to any parameters), the previous program would output:

Circle, Circle
Circle, Circle

Common Lisp, Dylan and Julia support multiple dynamic dispatch functions but do not support methods (single or multiple dynamic dispatch).

Would it be possible for a language to support multiple dynamic dispatch methods (instead of or in addition to multiple dynamic dispatch functions)?

I am asking this because supporting only multiple dynamic dispatch functions looks like a step backward to procedural programming. Encapsulation (implementation hiding behind an interface) is the cornerstone of object-oriented programming. Using functions in place of methods forces the exposure of the supposedly hidden states of objects to be able to manipulate them, because contrary to methods, functions do not have a privileged access to these states.

Géry Ogam
  • 602
  • 3
  • 13

2 Answers2

3

This is basically a fundamentally impossible problem:

The fundamental idea of multiple dispatch is that all arguments that take part in the dispatch are equal, i.e. that there is no argument which is "special". So, if you don't want a "special" argument, then you only have two choices: you either have privileged access to no arguments, or you have privileged access to all arguments.

The former case is what you are complaining about, but the latter case means that you get privileged access to objects that you don't own, thus breaking object-oriented encapsulation.

The fundamental idea of object-oriented encapsulation is that an object only has privileged access to itself. It doesn't even have privileged access to other instances of the same type, unlike Abstract Data Types. (That, by the way, is why instances of classes in Java are not objects, only instances of interfaces are objects.)

So, by the very definition of Object-Orientation, there can only ever be at most one "special" argument.

I remember reading a research paper on adding Multimethods to single-dispatch OO languages using so-called Tuple Classes. The idea was that you can define a class something like this:

tuple struct (Circle x, Circle y) {
  void overlap() { std::cout << "Circle, Circle" << std::endl; }
}

But the whole point of doing it this way was that methods in the Tuple Class (Circle x, Circle y) can only interact with the public API of x and y precisely because they are not defined by the original authors of Circle and thus shouldn't have privileged access.

Otherwise, I could simply define a Tuple Class

tuple struct (DoesntMatterWhatIPutHere x, SuperSecureCreditProcessor l33th4X0r) {
  void emailMeThePins() { std::cout << l33th4X0r.getPin(); }
}
Jörg W Mittag
  • 101,921
  • 24
  • 218
  • 318
  • Thanks Jörg for this interesting answer. You said "It doesn't even have privileged access to other instances of the same type." Are you talking about Java? Because [this is not the case in C++](https://stackoverflow.com/q/60216407/2326961) where members and friends of a type have privileged access to members of instances of the same type. – Géry Ogam Jul 22 '20 at 19:04
  • @Maggyero: This has nothing to do with the language. This is part of the definition of OO. In Java, the language specification calls both instances of classes and instances of interfaces "objects". But, those are only objects in the context of *Java*, going by the definition of OO, only instances of interfaces are *actually* objects. Instances of classes are *Abstract Data Types*, precisely because objects (in the Java sense) of the same class can call each other's private methods, whereas interfaces *cannot define* private methods, so as long as all references are typed as interfaces, all – Jörg W Mittag Jul 22 '20 at 19:09
  • "objects" are Objects. So, what you are basically saying is that instances of classes in C++ are not objects. – Jörg W Mittag Jul 22 '20 at 19:10
  • You are talking about "instances of interfaces", but I thought that interfaces (abstract classes) could not be instantiated, by definition. – Géry Ogam Jul 22 '20 at 19:18
  • 1
    `IFoo foo = new Foo()`. `foo` is an instance of `IFoo`, i.e. the *reference* to the object returned by `new Foo()` is typed as `IFoo`, and therefore, I can *only* access members that are defined in `IFoo`. Since interfaces do not allow the definition of state or private methods, this means that I can only use the public API of `foo`, and am therefore observing object-oriented encapsulation. If instead, I had `Foo foo = new Foo(); Foo bar = new Foo()`, then `foo` would be allowed to access `bar`'s internal representation and private API and vice-versa, meaning they are not objects. – Jörg W Mittag Jul 23 '20 at 05:15
  • I agree, my confusion came from the fact that abstract classes cannot be instantiated but can have instances. I realized that there is actually no contradiction: *cannot be instantiated* means *cannot create instances*, not *instances cannot be created*. Abstract class instances can still be created by a concrete subclass. – Géry Ogam May 14 '21 at 10:22
  • Coming back to your answer, what you say is that Common Lisp, Dylan and Julia which support multiple dynamic dispatch functions but do not support methods (single or multiple dynamic dispatch) are not object-oriented but procedural (abstract-data-type-oriented) since they break encapsulation by having privileged access to *all* their parameters instead of *one* (the receiver object)? – Géry Ogam May 14 '21 at 10:30
  • ‘The fundamental idea of multiple dispatch is that all arguments that take part in the dispatch are equal’ Yes but equal under *dynamic dispatch* (selection of an implementation based on the dynamic type of arguments), not under *privileged access* (access to the internal state of arguments) which is a different concept. To clarify, my question was about whether it is possible to have both dynamic dispatch on all arguments *and* privileged access to a single argument (i.e. encapsulation: the receiver argument can only access its own internal state, not the internal state of other arguments). – Géry Ogam May 14 '21 at 18:50
3

It's always possible

There are lots of work-arounds to implement multi-methods in languages that don't support them:

  • Some use dispatch tables. The most impressing is imho in Alexandrescu's Modern C++ Design where template magic is used to make it happen almost naturally... provided that you're very versed in template programming.
  • Some hijack the build-in single dispatch to make a double-dispatch happen. All you need is a "bounce-back call" in which each class has the responsibility of performing one level of dispatching. Google for double-dispatch in your favourite language and you'll get it.

Since there are known solutions to implement it, it can for sure be automated and build into the language.

It's just matter of cost and sacrifice

  • Performance: the more dimensions in the dispatch, the slower will the dispatch be. It's either because of more indirections or because of complexer searches in larger dispatch tables.
  • Design: this design is not in the mind of the Open/Close principle since you'd need to add a new implementation when a new partner-class is derived: you have to know in advance the possible interactions.

The design sacrifice of both the Open/Close principle, but also the principle of least knowledge is in my view the main reason why there are not much languages that offer what you expect.

Your example

Take the example of your overlap() and imagine there would be a way to add multi-methods (i.e. methods in the class and not free-standaing functions):

  • Imagine that you have implemented the Shape Circle Triangle and Retangle.
  • Imagine even that you've found a nice trick to take advandage of the symetry of the problem.
  • Your classes are fine and delivered in a library. Now the user of your library adds a new Shape, for example Octopus: how would this interact with your library: the user will not be able to change your library (close) - how can he/she extend it?

Conclusion : the solution should remain outside of the class

In your example, finding the overlap is in fact not the concern of one single class, but of two classes together. Adding a new class that intervenes in this dynamic, requires to define new couples of possible interactions. So in the end the interaction belongs not to the class, but to the context that knows about the potential classes in presence.

And if you consider the problem under this angle, the existing solution already offered for multiple-dipatch in C# seems to be the most promising to go.

Christophe
  • 74,672
  • 10
  • 115
  • 187
  • Modern C++ Design is such a difficult book. Mr. Alexandrescu must be incredibly versed in C++. – Andy Jul 22 '20 at 10:27
  • @Andy It works like magic. But like magic, it's difficult to understand. This is why I preferred to give a warning: "*...provided that you're very versed in template programming*" ;-) – Christophe Jul 22 '20 at 10:33
  • Thanks for this quality answer Chris (as always). I think I disagree with your statement that multi-methods break the law of Demeter since the law is about communicating only with *immediate* entities (like method parameters). However I perfectly agree that multi-methods violate the open–closed principle which is about being open for extension and being closed for modification, thanks to the use of abstract classes instead of concrete classes—which is not the case with the `Circle*` parameter of my `overlap` method. – Géry Ogam Jul 22 '20 at 18:44
  • This open–closed principle violation seems like a good argument for multi-functions over multi-methods. But the encapsulation violation seems like a good argument for multi-methods over multi-functions. So there is a tradeoff between extensibility vs. encapsulation. Do you share this view? – Géry Ogam Jul 22 '20 at 18:51
  • 1
    @Maggyero Yes, completely: the world is full of contradiction and we just have to find the right balance for the problems we are dealing with. Ideally the solution would be in multi-functions that rely on methods in order not to break encapsulation. However, this is not always possible, so we might from time to time have to rely on friends and privileged access. We then have to do it in a way to minimise the adverse impacts. – Christophe Jul 22 '20 at 18:57
  • That is exactly what I was about to ask you: couldn’t we have the best of both worlds by having single dynamic dispatch *methods* (for encapsulation) and multiple dynamic dispatch *functions* (for extensibility)? That way multiple dynamic dispatch functions would manipulate the state of its parameter objects *through the interface* defined by the single dynamic dispatch methods instead of manipulating the state *directly*. If it is possible, that is really a shame that Julia does not provide this (I am not sure about Common Lisp and Dylan but I think they do not provide this either)! – Géry Ogam Jul 22 '20 at 19:20
  • @Maggyero It seems that you are thinking of a multifunction implementing the template method pattern. In fact, this would be the design I'd be looking for first. However, I'm not sure it's always possible. Take the overlap between two random shapes, one being a traditional geometric figure, and the other a figure defined by a complex formula. While for a square or a circle, you could imagine, using some getters to respect encapsulation (some would nevertheless say you already leak internal implementation details), for the complex figure you'd need to know about its internals. – Christophe Jul 22 '20 at 19:26
  • That is a very relevant remark. This approach assumes that the interface of an object is powerful enough to let multi-functions that use it do their job, and as a result they could force the interface to provide methods that can access the internal state of the object, which may compromise its encapsulation. But this issue is actually not specific to multi-functions; any entity using the interface of an object, like methods of other objects, might need to access the internal state of the object, so I do not think that it is a serious issue. – Géry Ogam Jul 22 '20 at 21:22
  • 1
    @Maggyero: A good example is the concatenation of two doubly-linked lists. Doing it in O(1) requires privileged access to the `next` pointer of the tail of the first list and the `prev` pointer of the head of the second list. This is *fundamentally impossible* in OO. You *cannot* concatenate two doubly-linked lists in O(1) and at the same time observe object encapsulation. Cook calls operations that require inspection of two or more object representations at the same time "complex operations", and these are only allowed for Abstract Data Types, but not for Objects. – Jörg W Mittag Jul 23 '20 at 05:11
  • @JörgWMittag Interesting. Thanks for the reference, I am going to read William Cook’s papers. – Géry Ogam Jul 23 '20 at 08:17
  • Coming back to this topic today, I feel that we have been mixing two different concepts: *dynamic dispatch* (selection of an implementation based on the dynamic type of arguments) and *privileged access* (access to the internal state of arguments). To clarify, my question was about whether it is possible to have both dynamic dispatch on all arguments (i.e. multiple dynamic dispatch) *and* privileged access to a single argument (i.e. single privileged access, that is encapsulation: the receiver argument can only access its own internal state, not the internal state of other arguments). – Géry Ogam May 14 '21 at 19:39
  • You seem to answer yes and advocate method definitions (multiple dynamic dispatch *and* single privileged access) outside classes to avoid having to subclass, for easier extensibility. So this allows multiple dynamic dispatch while preserving encapsulation. And for the rare situations where a method needs multiple privileged access, i.e. access to the internal state of arguments other than the receiver argument (thereby compromising encapsulation), C++ already allows it for arguments of the same class and `friend` arguments. – Géry Ogam May 14 '21 at 19:39