Effective Dylan: 50 Specific Ways Dylan is Easier to Use Than C++


A point-by-point comparison between Dylan and the fifty items described in Scott Meyers' Effective C++.

I’ve been meaning to do this for a while. Here I go. The following is a brief† explanation of how Dylan compares against all fifty items listed in Scott Meyers’ Effective C++: 50 Specific Ways to Improve Your Programs and Designs††. Much as I like this book and find it useful for programming in C++, the fact is that a lot of what’s in it (and other books on C++) is about the unnecessarily hard parts of C++. Dylan generally provides a simpler and more productive (and enjoyable!) approach to programming.

†I wrote that before writing the bulk of this. Since there are, after all, fifty points, the overall result isn’t very short. However, my response to each item is relatively brief and may leave out a number of details that would be worth expanding on in the future.

††Note that I used the first edition of Scott’s book, although that link above points at the second edition on Amazon. At some point I’ll go through the second edition and update my comments if necessary.


Shifting from C to C++

This section is very specific to C vs. C++, so there isn’t a direct comparison in Dylan, but I’ll add some comments anyway.

1. Use const and inline instead of #define. Dylan has no preprocessor or #define. It has constants and inline functions.

2. Prefer iostream.h to stdio.h. Dylan has stream-based I/O libraries.

3. Use new and delete instead of malloc and free. Dylan has automatic memory management. You do not need to call functions to allocate and deallocate memory.

4. Prefer C++-style comments. Dylan supports both styles of C++ comments (/* */ and //), and common Dylan programming style agrees with Meyers.


Memory Management

Most of the items in this section are non-issues in Dylan, which has automatic memory management. There are no new and delete to use incorrectly.

5. Use the same form in corresponding calls to new and delete. An impossible error to make in Dylan. If you create a collection object, every object in it will be automatically deallocated as appropriate when the collection is deallocated.

6. Call delete on pointer members in destructors. Another detail that’s impossible to overlook in Dylan. All objects are properly deallocated when no longer referenced.

7. Check the return value of new. Dylan’s equivalent to new, “make,” signals an exception if object allocation or initialization fails. It is not possible to unintentionally overlook a failure.

8. Adhere to convention when writing new. There is no new.

9. Avoid hiding the global new. There is no global new.

10. Write delete if you write new. There is no…rule number six.


Constructors, Destructors, and Assignment Operators

These issues are dramatically simpler in Dylan. Automatic memory management eliminates the need to define constructors and destructors when the only resource being managed is memory. Initialization is much simpler in Dylan and in most cases can be defined in the class definition without a separate initialization function. There is no assignment operator function, and, in contrast to C++, Dylan doesn’t copy objects implicitly, and explicit copying is rare.

11. Define a copy constructor and an assignment operator for classes with dynamically allocated memory. Although you can define the equivalent of copy constructors when desired in Dylan, automatic memory management makes it unnecessary to worry about writing copying code merely to properly manage memory. There is no copying assignment operator in Dylan, and no implicit copying of objects, so it is rare that you need to write copying code; you do not need to do so simply to satisfy language semantics as in C++, you only need to do so when you wish to support explicit copying.

12. Prefer initialization to assignment in constructors. This actually applies to Dylan, although there’s less of a chance a Dylan programmer will make a mistake here. For most cases, object initialization can be expressed just once, in the class definition, unlike C++ initialization clauses, which cannot be used in as many cases and which have to be diligently written for every constructor. Dylan only requires an explicit initialization function for special cases, e.g., when multiple initial values have interdependencies.

13. List members in an initialization list in the order in which they are declared. A non-issue in Dylan, where initial values are given in the definition of each member (called “slots” in Dylan), so there is no way to get the order wrong.

14. Make destructors virtual in base classes. A non-issue in Dylan, where all functions are implicitly “virtual” (besides, Dylan does not have destructors in the C++ sense.) Note that this does not mean all function calls incur dispatching overhead, a cost many C++ programmers are very conscious of. If a given call site doesn’t require dynamic dispatch, it automatically becomes a simple function call, just as with non-virtual C++ member functions, and may even be inlined as appropriate. There is no need (or means) to explicitly declare “virtual” and “non-virtual” functions, and no class definition changes are ever necessary if program requirements later change.

15. Have operator= return a reference to *this. There is no assignment operator in Dylan.

16. Assign to all data members in operator=. There is still no assignment operator in Dylan.

17. Check for assignment to self in operator=. Well, there was one, but the cat’s eaten it.

(Someone asked me for further clarification: There is an “assignment operator” in Dylan, of course. It looks just like Pascal’s “:=”. However, it is not a function. It is not equivalent to C++’s operator=, which performs copying, and there is no need to implement one for your classes as in C++ simply to avoid problems with C++’s pervasive implicit copying.)


Classes and Functions: Design and Declaration

Meyers’ has some good things to say about class and function design that, broadly speaking, apply as much to Dylan as they do to any other language. However, a lot of these items are rendered simpler—or in some cases, trivial—to deal with in Dylan.

18. Strive for class interfaces that are complete and minimal. Okay, Scott, will do. In fact, in Dylan, class definitions are usually much simpler than in C++, only including definitions of slots (“data members”) and (implicitly) their accessors—an approach Scott recommends for C++, but which is made more difficult to adhere to than in Dylan because of the limitations of non-member functions.

19. Differentiate among member functions, global functions, and friend functions. Dylan has a simpler, more orthogonal programming model in which there is essentially only one kind of function (called a “generic function”), which is polymorphic, similar to a C++ virtual member function. Generic functions do not “belong” to classes, and there is no real distinction between “member” and “non-member”, or “friend” functions, making it simpler to evolve programs with fewer and more localized source changes. You use explicit modules (aka “namespaces”) to control access and encapsulation instead of implicit class namespaces and public, private, protected, and friend declarations.

20. Avoid data members in the public interface. In Dylan, accessor functions are automatically defined for all slots. In fact, there is no way to explicitly access a slot “directly” as in C++. All slot access is via accessor functions (calls to which are often optimized away in practice). In C++, you have to explicitly write your own accessors, complicating the code and increasing the likelihood that someone will avoid that work and make a data member public when they shouldn’t, and thereby make the code harder to maintain and evolve.

21. Use const whenever possible. Dylan doesn’t have const, which isn’t as clearly necessary as in C++. One reason for this is that, just as functions can accept multiple arguments, in Dylan they can also return multiple values, so you never need to design functions that take “output” pointer/reference arguments, and so there’s less need to differentiate them from “input” arguments. Dylan functions also tend to be functional rather than mutating (ie., they are implicitly const). Although Dylan explicitly supports imperative programming, most standard library functions do not alter their arguments, and so const-like declarations would often be redundant. (This is not to say there would be no value in having something like const in Dylan, but it isn’t as much of a clear win as in C++.)

22. Pass and return objects by reference instead of by value. Dylan always uses reference semantics. There is no direct way to pass by value, although you can explicitly copy an object then pass the copy along.

23. Don’t try to return a reference when you must return an object. See #22. This simply isn’t an issue in Dylan.

24. Choose carefully between function overloading and parameter defaulting. This applies only indirectly to Dylan. First, it’s important to note that Dylan does not have function overloading in the same sense as C++. Second, Meyers recommends overloading when there is no reasonable default value for an argument, but in Dylan it is always possible to define a default value (using type-unions, which I won’t go into here), and furthermore, it is possible to tell whether an optional argument was passed to a Dylan function, obviating the need for a default value if avoiding one is desired.

25. Avoid overloading on a pointer and a numerical type. In Dylan there are no raw pointers and therefore no null pointer, but more to the point, Dylan is more type-safe than C++ and there is no way to confuse a zero with a null pointer, or in fact any number with any value that is not a number. In stark contrast to C++, Dylan’s false, zero, and null character ('\0') are of distinct types, and there is no implicit casting between them as in C++.

26. Guard against potential ambiguity. There is no implicit casting and no ambiguity between construction and conversion; you have to explicitly invoke the desired function. Generic functions do not “belong” to classes in the same sense that C++ member functions belong to their classes, and so the potential ambiguity between two inherited member functions with the same name doesn’t have a direct analogue in Dylan. The closest match is name collisions in modules (again, aka “namespaces”); similar to C++, you do need to resolve any collisions that occur during importing, by excluding or renaming one or more of the offending names.

27. Explicitly disallow use of implicitly generated member functions you don’t want. The example Meyers uses is the default assignment operator. Dylan has no assignment operator, so that specifically is a non-issue. More generally, Dylan doesn’t automatically define any functions on your behalf that you would need to suppress (any undesirable behaviors are up to you to implement yourself).

28. Use structs to partition the global namespace. Here Meyers is suggesting a short-term workaround for the lack of namespaces in C++. Now that modern C++ compilers support namespaces, this is unnecessary. As mentioned above, Dylan has namespaces (called “modules”).


Classes and Functions: Implementation

Some of these points discuss design issues of a general nature that tend to apply across languages, but others highlight where C++ is more complex than one might desire, where Dylan provides a simpler and more enjoyable programming experience in contrast. In particular, Dylan has no header files and instead relies upon whole-program analysis, where the compiler can see all the source code for a program or library and isn’t constrained by a monolithic, compilation-order-dependent mechanism like C++’s translation unit.

29. Avoid returning “handles” to internal data from const member functions. As a general point, this applies to Dylan, and I’d modify this to the more broad, “avoid exposing implementation information through interfaces.” Note, however, that Dylan has no raw pointers, and that all slots are implicitly accessed via functions, so less raw data is exposed by default.

30. Avoid member functions that return pointers or references to members less accessible than themselves. This applies to Dylan in the same general way that #29 does, though, again, there is no way to return a raw pointer or reference to a member, making this (at least slightly) less of an issue.

31. Never return a reference to a local object or a dereferenced pointer initialized by new within the function. You can blatantly ignore this in Dylan, where it’s perfectly safe to return any object, since you’re always using heap semantics (ie., there is no way to return a reference a stack object). Note that I said heap semantics. Dylan programs are written as though objects are always allocated on the heap, but objects may be allocated on the stack if there are no external references to them. Dylan treats the location of an object as an implementation detail, and allocating on the stack is an optimization. Automatic memory management eliminates the problems seen in C++ due to incorrect references or omitted deletion.

32. Use enums for integral class constants. The opposite is true in Dylan: Go ahead and use constants, that’s what they’re for. The Dylan compilation model doesn’t use headers. Instead, the compiler simply looks at the definition of a constant regardless of where it is in the sources to find its value. So, Dylan doesn’t have the same scoping and compilation order problems that C++ does.

33. Use inlining judiciously. [Curly voice] Why, soitenly. In fact, Dylan compilers make judicious use of inlining on your behalf in many cases, and, again, Dylan doesn’t use headers, so you don’t have to place function bodies in headers to get them inlined. The compiler can inline any function in your program as appropriate, making inlining easier to manage with fewer changes to source code. Dylan compilers are also expected to perform certain kinds of inlining and partial inlining that you might not expect of a C++ compiler (or that might not be possible due to the language semantics and compilation model).

34. Minimize compilation dependencies between files. Of course, reducing dependencies is a good thing, but again, since there are no headers, the task is made much simpler in Dylan. No implementation details are explicitly exposed in header files. Only those dependencies that actually occur in your programs need cause recompiles, and only the affected functions need to be processed, rather than recompiling everything in a translation unit that directly or indirectly may depend upon a header file, as in C++.


Inheritance and Object-Oriented Design

Some of these items are general and apply to Dylan, but others are, again, non-issues in Dylan, or in fact the opposite of common Dylan practice. C++ imposes some design constraints that Dylan programs are not subject to.

35. Make sure public inheritance models “isa.” Dylan doesn’t have private inheritance, so you can ignore some of what Meyers has to say here, but the general point about inheritance modeling “isa” still basically applies to Dylan (and in fact, to all OO languages).

36. Differentiate between inheritance of interface and inheritance of implementation. A good idea in Dylan, too. Meyers goes on at length on this topic, and the summary is that Dylan is a bit simpler here because there are no non-virtual member functions, nor pure virtual member functions. To define an abstract class in Dylan, you do so explicitly with the adjective “abstract” in the class definition; there is no need to define a pure virtual member function merely to implicitly make a class uninstantiable.

37. Never redefine an inherited nonvirtual function. An impossibility in Dylan, as all functions use “virtual” semantics. This makes it simpler to modify and maintain Dylan programs without breaking client code.

38. Never redefine an inherited default parameter value. In Dylan it is perfectly fine to do so, whereas C++ semantics make this problematic. Subclasses can impose further restrictions and therefore it is desirable to alter default initialization values, and Dylan semantics make this trivial to do.

39. Avoid casts down the inheritance hierarchy. Because it is a dynamic language, Dylan does not have or need up- or down-casting. Furthermore, it does not have raw pointers or non-virtual functions, eliminating most of the issues raised by Meyers here. As he suggests, though, you should avoid relying upon the exact type of an object and instead provide one or more polymorphic functions that provide the desired tests for object attributes (in this case, the example is testing an object’s class, but this rule can be applied more generally).

40. Model “has-a” or “is-implemented-in-terms-of” through layering. Generally applicable to Dylan, too.

41. Use private inheritance judiciously. There is no private inheritance in Dylan, so this is a non-issue. More specifically, Meyers points out some cases where C++ requires using private inheritance instead of the more desirable layering approach. In Dylan, these cases simply do not occur, and layering is always applicable.

42. Differentiate between inheritance and templates. Dylan directly supports generic programming without the use of a specialized syntax like C++ templates, and it is considered good form to make the difference transparent. Most Dylan programming is generic programming to at least some degree, making it easier to reuse code. Dylan also supports homogenous and heterogeneous containers. In fact, unlike in C++, neither of these features requires generating duplicate code (although this can be done as an optimization, as a form of inlining).

43. Use multiple inheritance judiciously. Taking the topic title literally, it also applies to Dylan. However, MI in C++ has a number of complications that MI in Dylan simply doesn’t suffer from, making MI use easier and more common. Mixin inheritance is a common idiom in Dylan.

44. Say what you mean; understand what you’re saying. This is sort of a summary of some of Meyers’ previous points. I won’t bother going into detail. If you read his book and my comments, it should be straightforward to see how his points apply to Dylan.


Miscellany

These are general points, for the most part, and apply to Dylan, although the details differ.

45. Know what functions C++ silently writes and calls. Speaking very, very broadly, this is good advice for Dylan, too. However, in stark contrast to C++, Dylan writes and calls very little code implicitly, and any implicit code does the right thing (whereas, for example, in C++, if you have any pointer data members the default destructor will not call delete on them). In fact, Dylan really only generates accessor functions for slots, and they’re trivial. Default methods on make() and initialize(), which create and initialize objects, do the right thing by default. There is no copying assignment operator, copy constructor, or address-of operators as generated in C++.

46. Prefer compile-time and link-time errors to runtime errors. The same is true of Dylan, although unlike C++ you are guaranteed to get runtime errors (exceptions) instead of silent failures for things that can’t be checked at compile- or link-time.

47. Ensure that global objects are initialized before they’re used. This is guaranteed in Dylan. All globals (and locals, too) must have an explicit initial value. There is no way to forget to initialize one. Furthermore, Dylan makes it easier to specify initial values, and it guarantees that globals with dependencies will be initialized in the correct order.

48. Pay attention to compiler warnings. A good idea no matter what language you’re using.

49. Plan for coming language features. Most of Meyers’ points here are very specific to C++. I’ll just point out that one of Dylan’s greatest strengths is that it makes it easier to change things.

50. Read the ARM. Read the Dylan Reference Manual. It’s shorter and simpler than the ARM. To be fair, a lot of the “motivation” discussion in the ARM won’t be found in the DRM. Instead, look to other sources, such as info-dylan email and comp.lang.dylan Usenet archives, as well as Dylan Programming.

Posted: Sun - April 11, 2004 at 06:41 AM          


©