Why not C++?

There is a subset of C++ that is supposedly the bee's knees, according to Stroustrup, Sutter, and Dos Reis. Not to start an opinionated flame war, I want to know if you believe their argument holds enough water. I mean this both technically (if you stick to what they say, does it work) and in terms of practicality (are humans able to do that). To whatever extent you disagree, what are viable alternatives?

I mean this both about what they say works (their approach using 'good' C++), and what they say doesn't (cf. gc finalizers).

You can write C++ programs that are statically type safe and have no resource leaks. You can do that without loss of performance and without limiting C++’s expressive power. This model for type- and resource-safe C++ has been implemented using a combination of ISO standard C++ language facilities, static analysis, and a tiny support library (written in ISO standard C++). This supports the general thesis that garbage collection is neither necessary nor sufficient for quality software. This paper describes the techniques used to eliminate dangling pointers and to ensure resource safety. Other aspects – also necessary for safe and effective use of C++ – have conventional solutions so they are mentioned only briefly here.

The techniques and facilities presented are supported by the Core C++ Guidelines [Stroustrup,2015] and enforced by a static analysis tool for those [Sutter,2015].

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

My personal favourite pros & cons

(1) "Adding finalizers to GC partially addresses this problem but even something as simple and well-known as calling fclose() at GC time is almost always unsuitable. Finalizers:

* are executed at a nondeterministic time on a system thread, which can inject deadlocks with program threads;

* are not guaranteed to run at all, so there is no guarantee the file will ever be closed as long as the program is running;

* can lead to excessive resource retention when the GC is not run frequently; and

* can make GC unnecessarily expensive if a finalizer can place a reference to an object it was supposed to destroy in an accessible location (“resurrection”).

Consequently, the use of finalizers for resource cleanup is now actively discouraged in the major GC environments that support them, leaving the release of non-memory resources as a manual activity."

(2) "Using owner and static analysis is unmanageable for “ordinary C-style code”. We would simply need too many owner annotation so that those annotations would themselves become a nuisance and a source of errors. This has been seen in languages depending on annotations (such as Cyclone and Microsoft’s SAL annotations for C and C++ code) and our own experiments. To be manageable on an industrial scale, owner annotation must be rare. We ensure that by “burying” them in proper ownership abstractions, such as vectors and unique_ptrs."

(3) "As for dangling pointers and for ownership, this model detects all possible errors. This means that we can guarantee that a program is free of uses of invalidated pointers. There are many control structures in C++, addresses of objects can appear in many guises (e.g., pointers, references, smart pointers, iterators), and objects can “live” in many places (e.g., local variables, global variables, standard containers, and arrays on the free store). Our tool systematically considers all combinations. Needless to say, that implies a lot of careful implementation work (described in detail in [Sutter,2015]), but it is in principle simple: all uses of invalid pointers are caught."

(4) "Note that this is still local analysis; we just look at a function declaration with respect to possible invalidation. When invalidation is possible, we must be conservative and assume the worst. We consider whole-program analysis incompatible with the needs of large programs, the need for a fast debug cycle, and the needs of a language supporting dynamic linking. Whole-program can be useful, and we will use it, but not in the usual debug cycle."

(5) "This technique of moving objects rather than copying them has been used for decades, but only infrequently and unsystematically. The notion of direct language support for move semantics was pioneered by Howard Hinnant and is part of C++11. The use of move semantics allows us to move a large object implemented as a handle from scope to scope without overhead and without resorting to error-prone explicit use of pointers and explicit memory management. One implication of having move semantics is that we can completely encapsulate the management of non-scoped memory."

(6) "For most resources (memory, locks, file handles, etc.), acquisition can fail so a conventional resource manager must be able to report an error. The only fully general mechanism for that is throwing an exception if resource acquisition fails, and that’s what the standard containers do. This scheme handles nested objects, class hierarchies, and containers simply and efficiently. However, there are also schemes that rely on explicit error handling. Those are brittle and rarely generalize; the best basically hand- simulate RAII."

(7) "The model presented so far relies on sequential execution in a language with lexical scoping. In a multi- threaded system, we need to handle more cases. Our rule set for concurrency is not yet fully developed. In particular, detached threads with pointers to shared data can be tricky. However, threads that are joined at the end of their scopes can be analyzed much as called functions and shared_ptrs can be used to keep data alive for threads with less well- behaved lifetimes. There are also obvious opportunities for tools that analyze for race conditions and deadlocks."

(8) "Here, we will only briefly mention other ways of breaking the C++ type system. These problems are well known and have well-known solutions, so we will not address them here. Misuse of unions and casts can lead to type and memory violations (so follow the rules that prevent that [Stroustrup,2015]). For example, use a variant class rather than a plain union. Out-of-range access and access through a null pointer can lead to type and memory errors (so follow the rules that prevent that). In particular, use array_view and not_null from the Guideline Support Library (GSL) [Sutter, 2015b]. To minimize range errors, we also recommend using a make_array() function that returns an owner to allocate an array on the free store, rather than using new or malloc() directly. The aim of the Code Guidelines is to eliminate a large range of errors by mutually supportive rules. No one rule can by itself prevent a large class of errors: ban one misuse and others will become popular (this is often referred to as “playing whack-a-mole”). Thus, our ideal is a large set of mutually supportive rules that together deliver type and memory safety guarantees."

An amusing claim

This technique of moving objects rather than copying them has been used for decades, but only infrequently and unsystematically. The notion of direct language support for move semantics was pioneered by Howard Hinnant and is part of C++11.

Oh that makes me laugh! I don't know whether to attribute that to naivety or cognitive dissonance. How could Hinnant possibly pioneer something old enough and common enough to be actual "folklore"? And I don't mean the vague and wooly reference to "infrequently and unsystematically", only the cases that spring to mind of previous languages that had explicit distinctions between movement and copying of data to give the programmer ways of encoding ownership. There are so many, but just to cover a range of decades:

Ah well, I suppose C++ has achieved some sort of critical mass in mind-share that the state propaganda arm can freely churn out these sorts of claims.

On a more serious note - the report does describe a realistic approach to grafting ownership on top of C++. The templating trick looks like a kind of erasable tag that allows the static analyser to give a guarantee. The 1000:1 rule of thumb sounds like an issue though - how expensive is the tool in terms of compile-time?

The pioneering claim is in the context of c++.

Hinnant pioneered the development of language support for move semantics in C++. I do not think that Sutter et al are claiming anything else. In fact boost has had (multiple) pure library based move emulation in C++ since early 2000s and the standard library had (broken) ad-hoc move semantics support in auto_ptr since the initial standardization ('97-'98).

Quote is exact.

The quote above is lifted exactly from the report: although it is a quote from raould, he quoted it verbatim from the bottom of section 4.2.

They may have meant "in the context of C++" but what they have literally stated is that it is the first direct language support for move based semantics. Yes I was being picky (it's not been peer-reviewed), but what they have written does not unambiguously state what you have interpreted. Having said that - I would assume that you are correct about their intention.

move semantics

C++ does not have move semantics, it has a weak form of linear typing (namely rvalue binders) that would ensure transparent moving were the type system sound.

AFAIK rvalue binders are in fact novel, and IMHO, quite a useful and productive addition to C++.

The problem is that it adds *yet another* constructor or function argument to an already bloated set, instead of the compiler doing the analysis.

good subset should exist, but long docs are a problem

I sometimes get coworkers asking me what I think of C++ documents. I'll write another response later after careful reading. I just skimmed now to see what general flavor of discussion appears. So my first relevant warning, about the value of such documents, is that many developers are somewhat illiterate as a matter of final result: they can read but won't, and many parts don't stick. Sometimes this is a english-as-second-language issue, but even english-first speakers tend to read shallowly. An average dev would prefer I tell them what this document says, rather than read it themselves.

As a result, if success requires following a sufficiently large number of subtle rules, expressed in the middle of discursive documents, this won't happen. If the number of things you should not do is very large, that's too big a burden for many devs. A language with a long history has a disadvantage here, as the landscape is littered with things that will exhaust a dev's capacity for reading, as limited as it is.

Also, folks using C++ as relative beginners like to use as many features as possible, to establish a baseline for saying "yes I used C++ on a project". Why pick a subset when you can move from one technique to another in a single call chain? Every code base I see written by juniors has some part that looks like a parody of language feature coverage. Virtual functions, member functions, and event passing as variations on flexibility can occur in close proximity. How did this value get from here to there? Ha, ha! You're going to love this. Anyway, the injunction "use a subset" has some built-in incentive resistance.

That said, there should be a fine subset that works well. People famously pick different C++ subsets though. I would expect the experts to suggest a good subset with one specific flaw: individual parts of a vetted subset will have docs requiring enormous patience to grasp, and finesse to apply with skill. The subset will assume devs read hundreds to thousands of pages of docs, which is not a good assumption. Or junior devs will be expected to cut and paste with no idea what they are doing otherwise. Maybe that's the model.

My beef with C++ is not that a nice subset doesn't exist, but that folks won't stop steering continuously for change, so there's no stability to be had. And it's not hard to find apps written by folks who are no longer around, that used as many different things as possible. No one likes to hear that understanding what went wrong here requires being an expert in some boost libraries, just as a prerequisite to grasp program structure.

Anyway, it would be nice to talk about language subsets in general, to keep some general PL relevance, instead of having a C++ pro and con discussion. It's something for language designers to worry about. What happens when a language and ecosystem grow to the point that command of all of it cannot happen easily? How does a PL manage a focus friendly to devs who know just a core on a friendly ramp trajectory when learning?

Subsets

Yes, the document makes me think that I wish for all the stuff in the PDF to be encoded in a new language that compiles to C++. ;-)

Subset enforcement

It sounds like there is a tool that checks source for compliance.

resource management static analysis of references

Here's a more on-point reaction based on reading the manuscript in slightly more depth. Instead of summarizing the points, I may just categorize them, in terms of meaning in semantics space.

(The paper itself is low on summary, with no conclusion and a vague abstract about type safety and resource leak prevention. It begins with "there is a way to do something in C++", and then develops it a bit after motivation by code examples that introduce resource problems. The historical note looks suspiciously like a conclusion in structure.)

The most interesting thing shown is a static annotation of which pointers are owners, so proper ownership management can be analyzed, by tool. This occurs after prose explaining that dangling pointers are long term problem in C++: that you can see aliases to things whose lifetime you don't understand, making it easy to corrupt memory. The ownership analysis might remind some folks of related Rust features, but I won't comment on that, or conjecture which came first and influenced the other. (Some things are obvious and topical in a way concurrent exploration is expected instead of surprising.) Syntax involved looks like templates, and comparisons to smart pointers are made, without making a pithy summary observation.

I don't see a language subset identified by either detail or general rule, so that part seems an inference drawn from from context.

I personally like ownership expressed explicitly by some convention, preferrably one that is machine checkable. I'm inclined to do this in a C extension that compiles to C, by requiring that long-lived aliases occur only inside handles that refcount the referenced objects, and support auditing (with runtime cost when debugging). Brevity here is on purpose, as I don't want to write a long explanation, but it can be expressed without long documents (especially if you don't mind new terms). I'm not interested in selling the idea. You can do something like smart pointers in C, but you need a tool that statically verifies more than you can manage manually. It's crazy to have lightweight process actors without it. The model gets complex as handle flavors increase in number, specialized to accommodate weak and thread-safe semantics. But the basic one-flavor model sounds simple.

When I work on old code, I spend the most time on lifetime analysis when trying to make changes, followed closely by dataflow propagation which informs the lifetime analysis. So anything that makes this much cheaper has a big payoff in lowering code costs over time.

"When I work on old code, I

"When I work on old code, I spend the most time on lifetime analysis when trying to make changes, followed closely by dataflow propagation which informs the lifetime analysis. So anything that makes this much cheaper has a big payoff in lowering code costs over time."

have you checked the use of tools like http://frama-c.com/features.html

Using Frama-C to grasp source code internals

The C language has been in use for a long time, and numerous programs today make use of C routines. This ubiquity is due to historical reasons, and to the fact that C is well adapted for a significant number of applications (e.g. embedded code). However, the C language exposes many notoriously awkward constructs.
precise analyses
despite the pitfalls of C

Many Frama-C plug-ins are able to reveal what the analyzed C code actually does. Equipped with Frama-C, you can:

observe sets of possible values for the variables of the program at each point of the execution;
slice the original program into simplified ones;
navigate the dataflow of the program, from definition to use or from use to definition.

Proving formal properties for critical software

Frama-C allows to verify that the source code complies with a provided formal specification. Functional specifications can be written in a dedicated language, ACSL. The specifications can be partial, concentrating on one aspect of the analyzed program at a time.

The most structured sections of your existing design documents can also be considered as formal specifications. For instance, the list of global variables that a function is supposed to read from or write to is a formal specification. Frama-C can compute this information automatically from the source code of the function, allowing you to verify that the code satisfies this part of the design document, faster and with less risks than a code review.

--

like the idea of tools generally

I skimmed that one page just now, and may read more later when I have time. Analysis tools in principle sound good to me. But in some deployed software still under development, the scale and structure of code can make it almost infeasible to analyze nicely for several reasons.

Cross process async state changes in multiple languages ought to be hard to run through tools. Individually, it can be hard to tell what each process does. (Such is my experience.) A single distributed conversation is a challenge to piece together from start to end, especially with profoundly subtle sideways influence from concurrent config information. Code is a mixture in age from yesterday to twenty years ago.

Usually I am scaling things or adding features, but bizarre side mysteries sometimes need solving. Answering "what would happen if I change this?" can be pretty hard, especially with few specs, comments, or other clues about intent and structure. A lot of visualization is essential. At times it seems like card counting in a game with hidden card hands. For example, sometimes changing linear cost to constant time requires eliminating a detail that was either unreachable or disposable. It is seldom fun, but I succeed often enough.

It seems feasible to replace large complex things incrementally with smaller well-understood things, where tools tell you what new small things do, despite being stumped by original big complex versions. It would be like garbage collecting code, one algorithm at a time, until the old husk is replaced with a functional equivalent. But this is a crazy business proposition, because it departs far from status quo. You could cannibalize one process at a time this way, though, likely never reaching the end.

(I need to avoid being specific; putting a public face on things is outside my brief. This will be true of a lot of folks who might otherwise describe complex systems.)

There is a tool working with

There is a tool working with llvm that can modularize c and c++ code with good protection like a unix process but everything in one binary and process... could help to slice up old code and to upgrade parts of it.

My preferred subset is to reduce visual ambiguity.

There are a couple of different ways to "subset" something. The virtuous path is to use a subset that presents a cleaner and more semantically simple model - eg, if there were actually no difference for any semantic purpose between a reference and a value, then it does not matter which you passed as an argument, and if you identify a C++ subset for which that is really true, then all power to you in eliminating all visual distinction between them.

But with C++, in my limited experience, that never actually happens. Even if you can write something that way initially, it never stays that way. Inevitably the so-called "clean" implementation turns out to have bugs that you can't fix without going below that level of abstraction.

So, when using C++ now, I select a subset that reduces ambiguity as far as possible. IE, no pointers that don't look like pointers (eg, references), no things that look like pointers which aren't pointers (eg, "smart pointers"), no allocators that don't look like allocators (eg, things that get called for other reasons, which you might accidentally call from a finalizer), no finalizers that don't finalize (eg, 'resurrection').

Detecting errors far from the code that raises those errors turned out to be actively harmful. Hard experience has taught that whenever I call something that might throw an exception I must check the exception immediately when the call returns, exactly the same way I learned you must always check 'signal' return values immediately on return from calls into the C standard library. In fact it pisses me off that there are now two DIFFERENT ways to miss checking for errors and my code has to be cluttered up with both.

And if something can be done without using templates, I prefer to do it that way. I usually use the STL, but if and only if it does _exactly_ what I need as opposed to _approximately_ what I need.

C++ templates are 87% of a good idea, but if the fit is merely approximate, in ANY WAY, it fails. Any semantic friction, at all, is made intractable by the simple fact that it's implemented as templates and therefore any errors will provoke reams of gobbledegook which must be hunted through and understood in terms of subtle interacting details and then mapped back to the actual code you wrote, rather than a simple one-line error message that refers directly to the code you wrote.

It's kind of severe. I feel like I'm not getting much of the so-called "benefits" of C++. But I also feel like all those benefits
come with incomplete or flawed abstractions. I can't abstract away things unless I actually get sufficient assurances that I can stop caring about them.