X-Git-Url: https://git.distorted.org.uk/~mdw/sod/blobdiff_plain/cafa747a6fcdf4679287f3f00f24d26d134e14d3..fddbedf7b1b4b19add30eeb62281748cc77e6955:/doc/intro.tex diff --git a/doc/intro.tex b/doc/intro.tex index 64df8a0..67a220f 100644 --- a/doc/intro.tex +++ b/doc/intro.tex @@ -65,6 +65,192 @@ but Sod's ideology is different from that of most object systems. growing new `safety' features. \end{itemize} +\subsection{Comparison with other object systems} + +Sod's object system is significantly different in flavour\footnote{% + The pun was unintentional, but I'm happy with it.} % +from most popular object-ish languages. Indeed, it bears far more similarity +to Flavors, CLOS, and Dylan than to \Cplusplus, \Csharp, or Java (and still +less to Go or Rust). + +Of the popular languages, \Cplusplus's object system is probably closest to +Sod. Both are statically compiled, statically typed, implement single +dispatch, and have relatively simple runtime metaprogramming facilities. +\Cplusplus\ has a rich compile-time template metalanguage; Sod instead allows +compile-time metaprogramming in Common Lisp, which is probably less +convenient for simple cases but rather more pleasant for doing difficult +things. Significantly, Sod has the @|SodObject| class, of which all other +classes\footnote{% + Unless you construct a new root class of your own, which you can totally + do, but it's hard work.} % +are subclasses; \Cplusplus\ has no such root class. + +Both systems provide multiple inheritance, but go about it very differently. +The most important difference is that \Cplusplus\ provides only \emph{static + delegation}: if you have a class @|B| which defines some (virtual) member +function @|f|, and a derived class @|D| which wants to \emph{extend} the +behaviour of @|f| on instances of @|D|, then you must explicitly call @|B::f| +at the appropriate point: +\begin{prog} + \#include \\+ + % + class B \{ \\ + public: \\ \ind + virtual void f() \{ std::cout <{}< "B@\\n"; \} \\ + virtual @~B() \{ \} \-\\ + \}; \\+ + % + class D: public B \{ \\ + public: \\ \ind + void f() \{ B::f(); std::cout <{}< "D too@\\n"; \} \-\\ + \}; +\end{prog} + +This works adequately when only single inheritance is involved. But if we +now introduce multiple inheritance, we see the problem. +\begin{prog} + \#include \\+ + % + class B \{ \\ + public: \\ \ind + virtual void f() \{ std::cout <{}< "B@\\n"; \} \\ + virtual @~B() \{ \} \-\\ + \}; \\+ + % + class X: virtual public B \{ \\ + public: \\ \ind + void f() \{ B::f(); std::cout <{}< "X after@\\n"; \} \-\\ + \}; \\+ + % + class Y: virtual public B \{ \\ + public: \\ \ind + void f() \{ std::cout <{}< "Y before@\\n"; B::f(); \} \-\\ + \}; \\+ + % + class D: public X, public Y \{ \\ + public: \\ \ind + void f() \{ X::f(); Y::f(); \} // \comment{oh, dear} \-\\ + \}; +\end{prog} +The above prints +\begin{prog} + B \\ + X after \\ + Y before \\ + B +\end{prog} +which is unlikely to be what was wanted: `B' prints twice, and the `before' +and `after' actions are both in the middle.\footnote{% + Of course, one could have arranged to call @|Y::f| before @|X::f| -- but + the important point is that one would have needed to \emph{know} that this + was necessary. And you still end up with two copies of `B'.} % +The problem is that correctly composing behaviour from a collection of +superclasses requires knowledge of all of the superclasses involved and how +they're supposed to work together. Messing with virtual base classes has +eliminated the problem of duplicating @|B|'s state, but has done nothing to +help avoid duplicating @|B|'s \emph{behaviour} -- which is a shame, because +duplicating one without the other is going to end badly. + +The obvious workaround is to separate the functionality -- here, printing the +messages -- from the plumbing, which arranges to do everything in the right +order. You'd end up with a @|_f| member function in each class which wanted +print something, and then every class would have a virtual @|f| which calls +the various @|_f| functions in the right order, like this: +\begin{prog} + \#include \\+ + % + class B \{ \\ + protected: \\ \ind + void _f() \{ std::cout <{}< "B@\\n"; \} \-\\ + public: \\ \ind + virtual void f() \{ _f(); \} \\ + virtual @~B() \{ \} \-\\ + \}; \\+ + % + class X: virtual public B \{ \\ + protected: \\ \ind + void _f() \{ std::cout <{}< "X after@\\n"; \} \-\\ + public: \\ \ind + void f() \{ B::_f(); _f(); \} \-\\ + \}; \\+ + % + class Y: virtual public B \{ \\ + protected: \\ \ind + void _f() \{ std::cout <{}< "Y before@\\n"; \} \-\\ + public: \\ \ind + void f() \{ _f(); B::_f(); \} \-\\ + \}; \\+ + % + class D: public X, public Y \{ \\ + public: \\ \ind + void f() \{ Y::_f(); B::_f(); X::_f(); \} \-\\ + \}; +\end{prog} +This is clearly much more cumbersome. Most disastrously, it spreads +knowledge about how the various classes' contributions to the behaviour of +@|f| fit together throughout the class graph. Also, even this approach is +only suitable for especially simple cases. Suppose @|Y| needed to add +behaviour before \emph{and} after @|B| -- maybe @|Y| is taking out a lock, +and then releasing it. Then this approach won't work any more; indeed, it's +hard to see any way to make this work without spreading knowledge about +@|Y|'s lock everywhere. + +Compare Sod's approach. +\begin{prog} + code c: includes \{ \\ + \#include \\- + \#include \\ + \#include "foo.h" \\ + \} \\+ + % + class B: SodObject \{ \\ \ind + void f() \{ puts("B"); \} \-\\ + \} \\+ + % + class X: B \{ \\ \ind + void b.f() \{ CALL_NEXT_METHOD; puts("X after"); \} \-\\ + \} \\ + % + class Y: B \{ \\ \ind + void b.f() \{ puts("Y before"); CALL_NEXT_METHOD; \} \-\\ + \} \\+ + % + class D: X, Y \{ \} +\end{prog} +This prints +\begin{prog} + Y before \\ + B \\ + X after +\end{prog} +as (I think) you'd hope. @|CALL_NEXT_METHOD| here does the job of figuring +out what to do next, according to some rather complicated rules +(described in full in \xref{sec:concepts.methods.combination}). + +Indeed, there's an even better way to write this particular case with Sod, +because Sod has dedicated \emph{method rôles}. +\begin{prog} + code c: includes \{ \\ + \#include \\- + \#include \\ + \#include "foo.h" \\ + \} \\+ + % + class B: SodObject \{ \\ \ind + void f() \{ puts("B"); \} \-\\ + \} \\+ + % + class X: B \{ \\ \ind + [role = after] void b.f() \{ puts("X after"); \} \-\\ + \} \\ + % + class Y: B \{ \\ \ind + [role = before] void b.f() \{ puts("Y before"); \} \-\\ + \} \\+ + % + class D: X, Y \{ \} +\end{prog} + %%%-------------------------------------------------------------------------- \section{About this manual}