Home > Articles > Programming > Windows Programming

Interview with Andrei Alexandrescu (Part 2 of 3)

  • Print
  • + Share This
  • 💬 Discuss
Part 2 of this interview about the D programming language finds Eric Niebler and Andrei Alexandrescu deep in discussion about structs versus classes, the difficulties of copy semantics, rvalue references, the intricacies of garbage collection, and Andrei's occasional failure in serving as the standard-bearer for policy-based design.

See Part 1 and Part 3 of this interview.

Eric: D supports both value semantics (structs) and reference semantics (classes). One surprising thing about D's struct/class dichotomy is that the same syntax, t = u, does such radically different things for the two of them: for structs, it copies; for classes, it aliases. Does it hurt generic code and readability in general not to know whether t = u creates independent copies or aliases?

Andrei: Ha—good question! I'd actually rephrase your insight a bit. On the face of it, indeed, "copying" a class variable is really copying a reference, just like in Java, C#, and many other languages, whereas copying a struct variable in D copies the actual value—that is, each and every field. But this apparent dichotomy between reference versus value semantics is really a harmonious relationship between reference and custom semantics.

Structs are very flexible; they can be made to have reference semantics, value semantics, and everything in between. Consider:

// A bona fide D class
class X {
     private int x;
     int method(int y) { return x + y; }

// A struct with reference semantics
struct S {
     private X payload;

In the case above, S has de jure value semantics but de facto reference semantics: Copying S objects around really copies references to X objects. This is because S has exactly one field, and that field has reference semantics.

Seen from that perspective, no conflict exists. For some generic algorithm, you specify the expected meaning of each operation (notably that of copying objects around). If you clearly can't expect to work with reference semantics, it's very easy to eliminate classes from the matched subset of the type universe:

void willNotWorkForClasses(T)(T value) if (!is(T == class)) {

The if clause introduces a so-called template constraint. I proposed template constraints about a year ago as a lightweight alternative to concepts, and now I'm happy I did; they've solved a ton of difficult problems very elegantly.

To answer your question: Classes simplify things a fair amount, but structs offer a lot of semantic flexibility (not to mention efficiency). I see their coexistence not as a source of conflict and confusion, but instead as complementary harmony. You must have both in a multi-paradigm language.

Eric: With Mojo in C++[1], you addressed the issue of value semantics with efficient move. C++0x puts move right in the core of the language with rvalue references. D doesn't have them. What alternatives does it offer?

Andrei: The copy semantics as defined by C++98 aged really badly. I'm not saying that as a criticism—it's a very difficult problem! Mojo and other similar mini-frameworks are only palliative solutions to this nuisance. Most other languages chose to stay away from allowing user-definable copy semantics. Case in point: D version 1 chose to allow structs with (shallow) value semantics but not with hookable copying, just like C#.

When we designed D version 2, we were extremely careful to mark a net improvement from C++ in terms of copy semantics. We came up with a very simple and effective system:

  1. All objects are "relocatable"; that is, they can be moved through memory by using bitwise copying, à la memcpy.
  2. As a consequence of (1), the compiler never copies rvalues—it just moves them.
  3. Returning a stack-allocated parameter or a value parameter from a function is a move, not a copy.
  4. The library provides a simple convenience function move() such that move(value) reads value destructively and returns it as a (moved) rvalue.
  5. A struct can define a special hook function called this(this), also known as the postblit function, that the compiler calls against the target immediately after creating a bitwise duplicate of an object. The postblit scales better than the C++ copy constructors because you can add new fields without needing to adjust the postblit.

This system obviates the need for the devilishly difficult copy elision rules in C++ (one of my least favorite parts in that language's definition), and of course doesn't incur the hit of a new type constructor with its own rules and quirks, as rvalue references are. And, boy, are they quirky! Did you know that… bah, I'll save that for another day.

Again, this is not to criticize; I followed the rvalue proposal for C++ quite closely, and at one point I suggested a simpler scheme that Howard Hinnant proved to me would have broken compatibility (which is a no-no). To the credit of their creators, rvalue references achieve most of what they need to do, within an extraordinarily constrained setup. This trick reminds me of a scene in the movie Apollo 13, in which astronauts must build a sui generis air filter out of a plastic bag, a hose, a sock, and whatnot—except that, in the case of rvalues, only some unmentionables were available.

Anyway, getting back to D, the approach has a few more details, but the five points above convey the gist. The system works pretty darned well, is efficient, and requires no intervention (or only minimal intervention) from the user. The only disadvantage is ruling out internal pointers. But exceedingly few objects point inside themselves, and why hurt most for the doubtful benefit of a few?

Stacking D's uniform bitblitting of objects plus the postblit hook against C++'s copy-elision rules and rvalue references, I think D is making significant progress.

Eric: Rightly or wrongly, C and C++ performance hot-rodders are wary of garbage collection and its perceived performance penalty. What would you say to placate their fears?

Andrei: First, allow me to clarify the purpose of garbage collection (GC). GC is for writing safe programs with non-scoped allocation. If scoped allocation is all we need, we know how to typecheck programs for safety (Cyclone's regions and real-time Java are good examples); conversely, an unsafe program can perform unrestricted manual memory management. So again GC is for programs that a) need to be safe and b) use non-scoped allocation.

I'm rehashing this point because it's too often forgotten in arguments that frame GC as an indulgence, as the "easy way out" for languages and programmers alike. True, the infinite-memory model made practical by GC is easier to use, but the whole point of it all is memory safety.

Second, let's clarify that we're talking about a real cost, although the dimension of the cost isn't obvious. A 2005 study by Hertz and Berger, "Quantifying the performance of garbage collection vs. explicit memory management," has shown that real-world GC programs run about as fast as—and sometimes faster than—their deterministically deallocated exact equivalents, as long as they're allowed to occupy about 2–5 times more memory. When a program runs low on memory, relies on data locality, or must compete with other processes for memory, the overhead of GC grows and could become catastrophic. GC technology has improved since 2005, but with nothing earth-shattering, so I think that the above baseline holds.

Third, I should point out that many programs don't really care about garbage collection. RAM is plentiful, and few programs' core performance depends on data locality.

Where does D stand? D offers a garbage-collected heap used by default for class objects and built-in containers. If you want to write a safe program, just use new and you're there.

But you're asking about hot-rodding. If you want to fine-tune allocation, D's GC has a low-level interface that allows you to do unsafe things like freeing and resizing memory blocks. Furthermore, you can use malloc() and free(), along with the rest of C's standard library, and even the nonstandard alloca(), without any overhead. Then, a D primitive function called emplace allows you to construct objects at specified memory locations (à la C++'s placement new operator).

Most interestingly, D allows implementing memory-safe containers that internally use deterministic, unsafe allocation methods (for example, malloc() and free()), yet are encapsulated strongly enough to make any unsafe use impossible. I discuss that technique in depth in my InformIT article "Sealed Containers."

D applications still link in the garbage collector, and operations such as using new or concatenating arrays will use it silently. This is inconvenient for applications that need to make sure there is absolutely no use of garbage collection. Such applications can tweak settings to avoid linking in the garbage collector (which has a pluggable architecture). All uses of GC operations would translate in link-time errors, which is okay but not ideal.

Walter Bright is considering adding a compile-time flag that would banish all constructs that make implicit use of the GC, in which case you'll know at compile time where the culprits are, and you can change your code accordingly. Specialized library support à la boost::shared_ptr would be necessary. All that work hasn't been done yet, but it's well-trodden ground, so I don't foresee any difficulties.

Eric: Every garbage-collected language must grapple with the thorny issue of resource reclamation: Some resources need to be released deterministically, and GC is inherently non-deterministic. How does D deal with this issue?

Andrei: If there's a magic bullet, we haven't found it. Classes go on the garbage-collected heap; structs could go anywhere, but most often have a scoped lifetime. (As we just discussed, you could put anything anywhere with some effort; I'm talking about the path of least resistance).

The most difficult scenario here is a class that has a struct as a member. If the struct has a destructor, it will be run non-deterministically—or possibly not at all. Currently the D garbage collector calls all class destructors; but, as we know from other languages, it's best not to count on that possibility. If you need timely resource release for such embedded structs, you'd best do it manually.

All that being said, D takes certain measures that aim at simplifying matters:

  • The clear distinction between class and struct objects frames a design from day one, and it's a good statement of intent from the designer: "I define a class here, so I'm expecting an infinite lifetime model."
  • D distinguishes between destruction and deallocation. C++ conflates the two notions, which confuses a lot of people in a lot of ways. You see, memory isn't about just any resource; it's a very special resource. Unlike file handles, sockets, and mutexes, memory is the bedrock of the type system—everything that the language ever guarantees sits in memory. Close a socket, and you'll have errors reading from it—but no real harm done. Use a dangling pointer, and anything could happen—the type system is unable to hold any guarantee.
  • D defines for every object a primeval state in which it allocates no extra resources. You can put any object into such a state by evaluating clear(obj), which is a sort of "operator delete without the dangers." The universal availability of such a primitive makes it easy for generic client code to deallocate resources safely.

  • Finally, you don't need to use Java's awkward try/finally statements or the equally awkward C# using statement to clear resources in an orderly manner. To be brutally honest, I believe that both constructs are missing the point by a mile; to be brutally narcissistic, I believe that D's scope statement is a game changer. If you want to execute code upon a scope's termination, all you need to do is this:
  • auto wbdc = new WhizBangDatabaseConnection("wbdb://meh");
    scope(exit) clear(wbdc);

    It's lightweight, it's safe, it's deterministic. And it's a heck of a boon for reviewers, who won't need to follow complicated control flows. (You also get to execute code conditionally by replacing exit with success or failure.) I've been using this feature for years, and it scales phenomenally well.

To summarize my answer: D doesn't have a foolproof integration of GC and deterministic resource reclamation. However, it does offer a coherent framework facilitating resource control, and a statement that makes manual reclamation robust and effective.

Eric: I was surprised to hear that structs don't need inheritance—and from you, of all people, the standard-bearer for policy-based design! In many of your C++ designs, you use parameterized inheritance to customize the behavior of value types. Why don't you need this feature in D?

Andrei: Inheritance has at least two important purposes:

  1. The classic case is subtyping: "I want to inherit Button and tweak its behavior to allow an animated background."
  2. The other use is commonly called "inheritance of implementation": "I want to define Pool to offer what Factory offers, plus some other things." But I'd call this "symbol table acquisition," because sometimes no implementation is involved—think std::binary_function.

In C++, you'd want to use inheritance in both cases, mostly for practical reasons—you want the benefit of the empty base optimization (EBO), and you wouldn't want to write a bunch of forwarding functions. In D, you'd use classes in the first case and structs in the second.

Getting the benefits of EBO in D is very simple because of the static if construct. You see, if scope(exit) is a game changer, static if is a game enabler. I was very bummed that C++0x, for all its size, doesn't include anything as mighty as that. Here's how you avoid storing a useless member of type Factory inside an object of type Pool:

struct Pool(Factory)
     static if (Factory.tupleof.length == 0) {
         // Factory has no per-instance state
         alias Factory theFactory;
     } else {
         Factory theFactory;
     Object create() { return theFactory.create(); } }

tupleof yields the direct data members of a struct, so for an empty struct the corresponding tuple would have length zero. In that case, the code just creates a symbolic alias—theFactory is the same as Factory. Otherwise, Factory holds state, so you define an actual member. From here on, using theFactory.create() dispatches either to a static or a full-fledged member function.

If you want all symbols inside Factory to percolate through Pool's interface, you use a feature known as alias this:

struct Pool(Factory)
     ... as above ...
     alias theFactory this;

This feature works as you'd expect—if the compiler looks up a symbol inside Pool and doesn't find it, it continues down theFactory's symbol table. (Notice how I combined static if and alias this for compounded effect!) This behavior is really what you want.

And then there's general static reflection, with which people have done crazy things—search online for whitehole d language, and you'll find a class WhiteHole that takes another class and implements all of its abstract member functions to throw. Great for mockup testing and partial implementations!

Defining inheritance for structs (which is possible in a sound manner) might simplify certain scenarios, but it isn't an enabler. I'm not sure whether such a feature would pull its own weight.

Eric and Andrei wrap up their discussion of D in "Interview with Andrei Alexandrescu (Part 3 of 3)."

  • + Share This
  • 🔖 Save To Your Account


comments powered by Disqus