Sunday, December 5, 2010

C++ design goals in the context of Rust

Recently I came across Bjarne Stroustrup's often-cited list of design criteria for C++ again, and I thought it'd be interesting to compare it with my take on the design of Rust. Since Rust is meant to be used in many of the same scenarios as C++ is today, the design goals overlap, but Rust has a different focus that led to different choices. Note that this is my take only and not an official decree as to the design of the language by any means.

C++ is designed to be a statically typed, general-purpose language that is as efficient and portable as C.

Rust is designed to be a statically-typed, general-purpose language that is designed to be as efficient and portable as idiomatic C++, without sacrificing safety.

It's impossible to be "as fast as C" in all cases while remaining safe. As Dave Herman put it, "competing with C++ when you're safe is fighting with one arm tied behind your back". C++ allows all sorts of low-level tricks, mostly involving circumventing the type system, that offer practically unlimited avenues for optimization. In practice, though, C++ programmers restrict themselves to a few tools for the vast majority of the code they write, including stack-allocated variables owned by one function and passed by alias, uniquely owned objects (often used with auto_ptr or the C++0x unique_ptr), and reference counting via shared_ptr or COM. One of the goals of Rust's type system is to support these patterns exactly as C++ does, but to enforce their safe usage. In this way, the goal is to be competitive with the vast majority of idiomatic C++ in performance, while remaining memory-safe.

C++ is designed to directly and comprehensively support multiple programming styles (procedural programming, data abstraction, object-oriented programming, and generic programming).

Rust is designed to directly and comprehensively support multiple programming styles (procedural programming, data abstraction, object-oriented programming, generic programming, functional programming, and parallel programming). With the increasing popularity of dynamic languages like JavaScript and Ruby, functional programming has become a mainstream part of programmers' toolboxes, and we hope to fully support it. And parallel programming is becoming critical for good performance, and one of the main reasons for Rust's existence is that parallel programming is difficult in C++, especially with the traditional threaded shared-heap approach.

C++ is designed to give the programmer choice, even if this makes it possible for the programmer to choose incorrectly.

Rust is designed to give the programmer choice, but to guide the programmer to the best choice. You can use a mutable data structure in Rust, but you have to specify that in the type declaration, and you lose the ability to send such data over channels. You can use dynamic assertions throughout your code, but you cut down on check calls by performing the assertions as early as possible and propagating the constraints down with predicates. You can use unsafe code, but you have to mark the functions using it as unsafe and mark the associated modules as unsafe in the .rc file. Rust isn't intended to be a "bondage-and-discipline" language, because writing code in the recommended style is designed to be as straightforward and friendly as possible, but it is designed to make the programmer aware of aspects of the program that could have a negative impact on safety, performance, or correctness.

C++ is designed to be as compatible with C as possible, therefore providing a smooth transition from C.

Rust is designed to be as compatible with external C code as possible, therefore providing a smooth transition from C. The runtime features a separate C stack that's very quick (8 instructions) to switch to, and destructors allow C resources to be managed like Rust resources. The source language is obviously incompatible with C, but every language needs a good foreign function interface (FFI) to survive. We're already making heavy use of the FFI in the production compiler to interface to LLVM.

C++ avoids features that are platform specific or not general purpose.

Rust attempts to be as portable as possible, but we aren't particularly conservative regarding the standard library. Instead, the goal is to adopt a "batteries included" model à la Python. We diverge significantly from C++ here; we don't want different programs and libraries inventing things like vectors and hash maps.

C++ does not incur overhead for features that are not used (the "zero-overhead principle").

Rust incurs minimal overhead for features that aren't used. "Zero overhead" is too restrictive in practice, as, taken literally, it forbids features like garbage collection. Rather than aiming for zero overhead, I hope to achieve two goals: first, to make the runtime system simple enough that programmers aren't surprised by its behavior; and second, to make the system conservative enough that most software projects use most of the features anyway. Most commonly in large C++ projects, the "zero-overhead principle" is important with respect to two features: exceptions and runtime type information (RTTI). In Rust, exceptions don't exist (we need stack unwinding, but it's simpler than that of C++), and runtime type information does in fact conform to the zero-overhead principle (although runtime type information will be frequently used in practice due to the standard library's implementation of collections).

C++ is designed to function without a sophisticated programming environment.

Rust is also designed to function without a sophisticated programming environment. Rust crates are self-contained modules in the operating system's native format. There are no runtimes for end users to download beyond a small rustrt.dll runtime and the standard library.

Ultimately, Rust and C++ have more in common than not, but the few different design decisions buy us radically different safety, concurrency, and (we hope!) usability properties.


Benoit Jacob said...

I would like to comment on this:

> 'Most commonly in large C++ projects, the "zero-overhead principle" is important with respect to two features: exceptions and runtime type information (RTTI).'

I would add garbage collection to this list of costly features that often aren't required even by large projects.

I would also add one more angle of comparison to C++, generalizing that: to an experienced C++ programmer, C++ code translates trivially and naturally to assembly, modulo details such as register allocation. While, for example, in a GC-based language, the GC code is an arbitrary black box.

Patrick Walton said...

Benoit: That's very true, and it's why we're trying to keep GC down to a minimum. Garbage collection is only used by mutable data that lives on the heap (and we may even be able to prune the set of objects down further: there's been talk of confining it only to recursive tags). The idea is that where the type system can prove that cycles can never form, we need no garbage collection. Garbage collection is only used where cyclic data structures could form on the heap.

Benoit Jacob said...

Thanks for the reply, that's very interesting.

> Garbage collection is only used by mutable data that lives on the heap

ah so IIUC stack object are free of overhead there, cool.

So are stack objects also free of reference counting and any other mechanism that incurs any overhead?

Going one step further, does this mean that Rust can optimize away stack variables in the way that c++ does? A simple example is if I have a class Number that wraps just a float, and define operator+ and operator= in the trivial way in this class, then any decent optimizing c++ compiler will optimize
Number a,b,c,d;
a = b + c + d;
in the natural way, i.e. will optimize away the temporaries returned by operator+ here (crucial in this example is the fact that Number's fit in registers).

I'm not asking if the Rust compiler is currently able to perform this optimization, I would like to know if the Rust language theoretically allows that.

Patrick Walton said...

Benoit: Yes, stack objects are free of any memory management overhead. The only drawback is that, at the moment, not everything can be a stack object. (Objects can't be, for example, but we're investigating static binding forms so that you can get things that look and feel just like objects but don't give you a performance penalty.)

As for the optimization you mentioned, we don't have operator overloading, and objects always live on the heap. But if you had a number tag, like this:

tag number { f32; }

And you had an addition function:

fn add(&number a, &number b) -> &number {
ret a + b;

Then you could do:

auto a = add(b, add(c, d));

And LLVM should optimize away the tag via scalar-replacement-of-aggregates and perform inlining to make this equivalent to the addition of direct floats.

If we had static binding forms (which is looking increasingly likely), then it would look like:

auto a = b.add(c.add(d));

Which would be equivalent to the above in semantics and performance.

Caspy7 said...

This clarifies more about Rust to me, but one thing I'm still fuzzy on is how Rust fits into the context of Mozilla.
Perhaps it doesn't or this question has been answered previously, but I see that this is being pushed to the Mozilla Planet blogfeed.

Is there thinking that some Mozilla technologies, such as Firefox, might someday compile in Rust?

Benoit Jacob said...

Ah ok. So if I understand correctly, Rust has a special restricted kind of classes called 'tags' whose objects can live on the stack and be completely overhead-free and optimized; but more general class objects can't. Do you have a link on 'tags' so I can understand how much they allow to do?

What I had in mind, fwiw, was the overhead-free domain-specific embedded languages, as C++ allows. For example, i'm developing a C++ matrix library, and C++ (using expression templates) allows me to define a domain-specific embedded language for them, controlling how expressions such as a*b+c are evaluated, without any runtime overhead. What I'm trying to understand is how much of that could be achieved in Rust.

Patrick Walton said...

caspy7: This explains it better than I can:

Benoit: Tags are just sum types/discriminated unions. See the documentation here [1] or the wiki article [2].

I think what you want is a syntax extension; i.e. a macro. I've been discussing the idea of building Scheme-style syntax-rules or Dylan-style macros on top of syntax extensions to give programmers an easy way to define simple macros. Our macros should be far more powerful than C-style macros and should allow you to define little DSLs.

I envision our macro system as the equivalent to C++ templates. It seems to me that, with what people do with Boost and the like, that what people really want from a systems language is a full-fledged, modern macro system.


Benoit Jacob said...

Thanks again for the explanation. syntax-rules would indeed be quite awesome, and are indeed one of the main uses for template-based metaprogramming in c++. I will look into rust's macros system, I especially hope it's turing-complete like c++ templates are.

Anonymous said...

Don't count GC as a pessimizing feature so quickly. A good GC system can make memory allocation effectively free, and amortize the cost of reclamation.

Patrick Walton said...

anonymous: Different systems have different requirements. Rust is a conservative language, so the default implementation prefers reference counting to garbage collection where possible.

It's quite possible that we could experiment with a setting in the future that just GCs all heap data and compare performance (in fact, we've discussed this briefly). It'd be very interesting to compare the two approaches and get some performance numbers out of it.

Dave Hodder said...

At risk of stating the obvious, Rust omits many C++ keywords, such as class and public/protected/private. I'm intrigued by Rust, and would love to see side-by-side examples with code fragments from each.

Presumably idiomatic Rust would be termed rustic, with non-idiomatic code termed unrustic. ;o)