Let`s start by dispatch. In some OO terminology, objects encapsulate state and only communicate to each other via messages. Using that terminology, it becomes more easy to grasp what "dispatch" is: sending the message to the right destination. Polymorphism essentially means different objects responding in different ways to the same message.
The message is what we most commonly call "invoking a method". So dispatching is about what happens when you invoke a method. In the case of static dispatching the recipient of the message is resolved during compilation. That means that the next execution point will be hard-wired already in compilation time.
In dynamic dispatching when you reach a certain execution point where you invoke the method, it will be resolved at runtime which execution path to take next. That is definitely more flexibly, but it does not take much analysis to understand this cannot be implemented as efficiently as static dispatching.
- C++ : static dispatch is used by default. By defining a method as virtual, you enable dynamic dispatching. Still only applicable if you call objects via a pointer, or reference.
- Java: dynamic dispatching
- Rust: at client code side you need to mark as dyn to use dynamic dispatch (I like this - very explicit when you want to use it or not)
- Smalltalk: never wrote a line on it, but I always read it also natively has double dispatching (dispatching depends on the types of both communicating objects). This is achieved in other languages by the visitor pattern.
Not quite. As described above, you can opt-in to have dynamic dispatch with traits. Also, the contrast you make only makes sense if the language support both types of dispatching. So I will stick to comparing Rust to C++ here. C++ inheritance you do not automatically move into dynamic dispatching - only virtual methods do.
With inheritance what you get is:
- Run time polymorphism , if you apply dynamic dispatching
- Access to protected attributes and methods
- Storage of the attributes - so memory use inheritance is also a mechanism to extend the data you store
(this last point is what leads to diamond hierarchy problems)
In Rust does not have protected attributed/methods, and you get no attributes/memory storage from implementing a Trait.
Over the years C++ programmers turned more frequently from runtime polymorphism, achieved via inheritance to compile-time polymorphism achieved with templates (generics), for being more efficient. In C++20 this finally has reached a new level with Concepts, which look a bit similar to Traits. But still, you need to decide whether you want to model something as Concept and work solely with compile-time polymorphism or as class and work solely with run-time polymorphism. In Rust Traits I liked that you get compile-time polymorphism by default, but you can opt-in to run-time polymorphism should you need the flexibility attached to runtime resolution.
So I see a Trait as something you can choose to use either as a Java Interface or a C++20 Concept depending on the context. I like it