
A brief introduction to C++'s model for type- and resource-safety [pdf] - signa11
http://www.stroustrup.com/resource-model.pdf
======
pjmlp
I love the safety path Bjarne, Herb and others are driving the language, but I
wonder how much of this work will ever be adopted by the masses, given the
amount of "C with C++ compiler" that I usually see.

Or code guidelines like the ones from Google, where one is even refrained from
using Modern C++ best practices.

~~~
hellofunk
What Modern C++ practices specifically have you found that Google discourages?
I'm curious. I'm reading through Scott Meyers' Effective Modern C++ and am
finding so much easy and fantastic advice, I wonder if Google is at odds with
some it?

~~~
otabdeveloper1
> I wonder if Google is at odds with some it?

It's at odds with almost all of it. Google's standards are C-with-classes
based on compiler bugs circa 1999.

Despite their (misleading) marketing, Google is still just another enterprisey
megacorp employing 30000 blub programmers. Expect the usual 'enterprise'
failures.

~~~
_yosefk
> Google is still just another enterprisey megacorp employing 30000 blub
> programmers. Expect the usual 'enterprise' failures.

Ouch! That must have hurt 'em. Of course the original meaning of "Blub" is
"anything which isn't Lisp", _especially_ C++ (as well as say Java.) But what
the heck, it has a good sound to it.

Anyway, I think their C++ style guide isn't bad. As to "modern C++ style",
let's wait for a few years, shall we? When the STL came out, auto_ptr and
for_each(v.begin(), v.end(), FunctorFromHell()) were marketed as the way to go
for a long while; then the usual suspects became a bit more silent. Now again,
despite their being a smart for loop, for_each with a lambda (with its crazy
capture lists and all) is supposedly considered at least as good as a for
loop, and typically better. And you're supposed to use unique_ptr and what-
not. Maybe a few years down the road things will look different again.

C++11/14 has a ton of weird shit you ought to keep in your mind on top of the
weird shit in C++98/03 and many "enterprisey megacorps" seem to apply a
restricted subset of C++ rather successfully.

~~~
hellofunk
It's hard to deny that there are some really useful new features in modern
c++, though. Standardized multithreading primitives is a big one. Smaller ones
like scoped enums are useful too. I'm a big fan of lambdas and the ability to
use them for things that used to be very awkward to do without them, like
std::bind and functors. In all, I think the modern additions are a net
positive for sure.

~~~
_yosefk
I don't deny that there are useful features in C++11/14 (though I find "modern
C++" an irksome marketing trick by people peddling a disgusting programming
language; it was called "modern" since at least the early 2000s and it was
disgusting then.) Of course I'd rather iterate over std::map with a range for
loop than use the C++98 way, and as to lambdas - while I find them way too
awkward and limited compared to closures without the sharp edges (and with
garbage collection), and _a ton_ of "modern C++ code" went batshit insane in
its extent of unnecessary reliance on lambdas, lambdas remain much more
practical than the alternatives when doing things like some sort of a
parallel_for. But then it's hard to find a language whose version X+1 adds
nothing useful to version X.

Is C++14 a better language than C++98? It's hard to tell (I think it is - I
think C++ is worse than C, but having gotten to C++98, perhaps it helps more
than hurts to add all that other shit), but it's not an interesting question
anyway because the upgrade is easy enough and it's worth it for most people
for their specific reasons, so C++98 will die out.

But then it's not like Google aren't upgrading to the latest C++ standard,
they're just doing it slowly AFAIK.

~~~
hellofunk
It's interesting to consistently find that people who hate C++ just really
really hate it a lot, to the point of using profanity a lot of the time.

I don't see what the big deal is. C++ is probably the most gigantic and
complex language I am likely to ever use... but you don't have to use the vast
majority of it if you don't want to. If you want to stick with C-style coding,
you pretty much can, if you don't want OO or STL or ++11/14 features --
nothing is forcing you to write those features into your code. But when you
want access to them, there they are; and if suddenly you find yourself needing
a particular library that only exists in C++ (as so many graphics and physics
engines do), you can get access to that as well.

As soon as you need multithreading, or sorting of hash maps, or even basic
string operations, knowing you can reach for them in pinch rather than find
the C alternatives or build your own, seems like a big bonus to me (assuming a
comparison to C).

~~~
72deluxe
True, but I think a lot of the encouragement is to move _away_ from writing
C++ like it is C because we really don't want to encourage people to write
with dangerous approaches anymore (like you can if you write C++ like it is
C).

I work with a few who write their C++ like it is C (pointers thrown around, no
const-correctness, memcpy instead of copy constructors etc. etc.) and it is
horrible to read. There is no indication of lifetimes of objects, nor any
indication of ownership. No RAII, no exceptions, nothing. It is dangerous.

You are rewarded for writing C++ safely because you won't have to fix any bugs
and you can offload all the work onto the compiler for checking types and
safety for you.

Like you, though, I don't understand people who get red in the face about C++
and go into a profane rage. It is just a language after all.

~~~
ratboy666
Guilty. Of writing C++ as if it were C. Sometimes even with classes. Yes, I
use memcpy instead of copy constructors. I find "modern C++" horrible to read.
Indication of object lifetime? Ownership?

Um... why? If the development support does not include a garbage collector, I
have to be very careful about these things. And, I am. Very careful indeed. I
just use the approach I do in C, and assume NOTHING from C++. Honestly, I do
NOT assume that the language system, compiler or run-time can do it properly.
I have been bitten too many times.

I do presume a garbage collector at the very next level up.

Yes, you may find my code "horrible to read". And, yes, I have gone into
profane rages over C++ code. It happens when the code bullies me -- with
hopeless error messages for simple typos. When simple changes cause the
application to no longer link. When simple changes cause massive explosions in
object size. In fact C++ is so complex, it encourages cargo cult programming.
Not just use of libraries, but enforced cargo-cult when USING the libraries.
Because, raisons.

Not just a language -- hell-spawn. Sorry, "modern hell-spawn".

~~~
vvanders
If you hate the idea of ownership and lifetimes I want to stay the hell away
from any codebase you work on.

~~~
ratboy666
No, what I "hate" is the idea that C++ promotes "automatic" management of
"ownership and lifetime" without a garbage collector.

Which works, except when it doesn't. Which results in stuff like reference
counting in the codebase. Which, of course, changes on a whim.

Example: the CEF3 library (chromium embedded framework).

Reference counted objects. Which requires source code changes depending on the
version of CEF 3 being used. Requires VERY deep analysis to determine what to
change in the using code. Really -- because the objects participate in a
multi-threaded library.

So, most code ends up as "cargo cult".

Since my code doesn't fit into the CEF3 object model, I do the contortions
needed -- building another layer.

~~~
vvanders
Ownership and Lifetimes != Reference counting.

Reference counting is code smell that you _don 't_ have a clear ownership
model.

Take a look a Rust for something more along the lines of what I'm talking
about. There's been quite a few people who've re-written their C/C++ code to
Rust only to find out that they didn't have a clear ownership model and found
architectural improvements because of it.

------
pcwalton
Here's my biggest question posed yet again, because the paper does not provide
an answer to it: The analysis requires that all mutable references be
statically proven to not alias on function entry. So how do you call a
function foo(T& a, T& b) as foo(x, y) where x and y are instances of
shared_ptr<T>? (The problem persists even if a and b are of different types,
because a might own b or vice versa.)

This is not a contrived example. It is precisely what kept hitting us in Rust
with things like @mut and we took years to figure out the best solution
(RefCell/Mutex). It comes up all the time because you can't get away from RC--
large programs use reference counting a lot, and even if they don't one of the
major use cases of systems languages is to interoperate with GC'd or RC'd
systems (dynamic languages, COM, Core Foundation) where you don't statically
know anything about aliasing. Forbidding these calls statically bifurcates
functions into "those callable on shared_ptr" and "those callable on owned or
stack-local objects", which is unusable in practice. The only solution we
found is _dynamic_ enforcement of the aliasing rules, scoped to reference
counted pointers. This indeed works, but it requires overhead. You want this
overhead to be opt-in in a low-level systems language, but existing code was
not written to opt into this. So C++ will need to backwards incompatibly
introduce RefCell/Mutex to support this.

The bottom line is that I see a dilemma. Either C++ makes it impossible to use
shared_ptr with mutable references, severely damaging usability, or it breaks
backwards compatibility by making shared_ptr immutable and requiring dynamic
enforcement of the borrow rules for mutable shared_ptr. Either option has huge
consequences for existing C++ code.

------
greydius
Safe by convention is a ridiculous notion. I think (and I know this isn't a
popular opinion here, but I'll say it anyway) that C and C++ are dying horses;
these 'modern' versions and 'best practices' are not adequate solutions,
evidenced partly by the fact that we can't even agree on what they should be.
The next decade is going to see the rise of systems programming languages with
static safety guarantees. Personally, I think Rust has a lot of potential, but
we will see.

~~~
cousin_it
I wanted to write a similar comment, but then noticed that the paper claims
100% safety by using an analysis tool, not just by convention.

Agreed about the potential of Rust, it sidesteps most of these problems.
Though it might introduce other problems of its own, only time will tell. I
expect that Rust's future sore spots will include destructor leaks, the
reliance on macros, and the tremendous complexity of traits. (Though of course
I don't claim to have better solutions to those.)

~~~
kibwen

      > destructor leaks
    

Rust doesn't leak destructors any more than C++ (including the version of C++
presented in the OP). The only way to leak a destructor accidentally is to
create a cycle (which is quite difficult to do, essentially requiring you to
stuff a RefCell inside an Rc) or to have some data owned by a thread that's
eternally stalled.

    
    
      > reliance on macros
    

Individual macros are generally regarded as convenient workarounds rather than
ideal long-term solutions, and anywhere that you see pervasive macro use tends
to be ripe for an RFC for a dedicated feature to subsume it.

    
    
      > the tremendous complexity of traits
    

No denying that traits are Rust's "deepest" feature, but that's because of a
general philosophy to leverage traits to provide newly desired bits of
functionality rather than introducing all-new language concepts. Removing
functionality from traits wouldn't result in making the language less complex;
that functionality would just end up going elsewhere.

~~~
cousin_it
> destructor leaks

Right, I'm not saying C++ is perfect either. I feel like Rust made a wrong
turn here:
[https://www.reddit.com/r/rust/comments/3404ml/prepooping_you...](https://www.reddit.com/r/rust/comments/3404ml/prepooping_your_pants_with_rust/)

> reliance on macros

I'm referring specifically to try!.

~~~
Manishearth
Note that whilst the leakpocalypse made it _safe_ to leak, it's not a common
case. Leaking safely involves an explicit call to mem::forget or extremely
convoluted code involving Rc cycles. Its pretty much going to happen in
languages which provide refcounted pointers.

None of the proposed solutions mitigated this either. You will always be able
to leak cycles. The difference was that some proposals made it possible to
mark types as non-leakable, in which case they can't be placed in things which
may potentially leak.

The fact that things will leak was never under question there. The decision to
be made was on how safety should interact with it -- should there be a way for
a type to rely on its destructor being run for the purposes of safety? The
decision was that there shouldn't, and we ended up fixing two APIs which
relied on destructors being run for safety, instead of adding a major language
change.

~~~
cousin_it
For what it's worth, I feel that marking types is also a mistake, and the
whole machinery of OIBITs exists due to some unfortunate decisions made early
on.

Maybe the cleanest solution to the leakpocalypse would be to mark
RefCell.borrow_mut as unsafe, because it's problematic in two separate ways
(it can both panic and create cycles). That way the safe fragment of Rust
would guarantee that memory leaks can't happen and destructors always run.
That would fit with the spirit of Rust, which strongly prefers 100% guarantees
to 99% guarantees. Though I guess that idea was judged too inconvenient for
users, and I don't claim to know what users want.

~~~
dbaupp
Concurrency/parallelism is sometimes described as "abstraction piercing", i.e.
you can't tell if something is safe for use in a concurrent code by just
looking at its interface, the internals of the type and the implementation
details of its functionality matter.

One way to handle this is to have each type explicitly opt-in to saying
they're concurrent-safe, but this is fairly annoying, as it requires a lot of
boilerplate. Boilerplate that is mostly pointless in Rust, because its rules
mean _most_ types people define will be safe, and most types that aren't safe
will be built out of existing types that aren't safe. OIBITs are proxies for
"safe", and are designed to strict a balance, by basically following that rule
(types made out of types implementing an OIBIT automatically impl that OIBIT
too).

Moving on to your specific suggestion: RefCell isn't the only way to not run
destructors, for instance, one can create ownership cycles with Mutex and
RWLock in a similar way, and, more broadly, threads that dead lock (or live
lock) will leak their resources, as would calling std::process::exit. There's
also weirder/wilder things like the following: suppose Queue<T> is a multiple-
producer multiple-consumer queue type, where any instance can act as both a
producer and a consumer (typical for such types), then the following code
creates an ownership cycle within the queue, and so won't run the destructor
of X:

    
    
      struct Msg {
          x: X,
          y: Queue<Msg>
      }
    
      let q: Queue<Msg> = ...;
      let q2 = q.clone(); // another handle to the same queue
      q.send(Msg { x: ..., y: q2 });
      drop(q);
    

Now, sure you could mark all these things unsafe, but I think that starts to
make things very inconvenient, and, somewhat, weaken what `unsafe` means (it
normalise its use since those things are typically totally fine). (Also, those
examples are almost surely not exhaustive.)

~~~
cousin_it
Interesting! Do you mind if I ask you a few newbie questions?

1) After some soul searching, it seems like the really important invariant to
me is that all alive objects should be reachable from the stack(s) at any
given moment. So I'm kind of okay with failing to run destructors due to
deadlock or process exit, but not okay with leaking a cycle. Is that a
sensible distinction, or is the situation even more subtle?

2) How do I make a cycle with Mutex and RWLock?

3) I don't completely understand the queue example, what exactly is the magic
that allows self-reference?

~~~
dbaupp
_> 1) After some soul searching, it seems like the really important invariant
to me is that all alive objects should be reachable from the stack(s) at any
given moment. So I'm kind of okay with failing to run destructors due to
deadlock or process exit, but not okay with leaking a cycle. Is that a
sensible distinction, or is the situation even more subtle?_

I think that's a reasonable distinction, but, at least for deadlocks, I'm not
sure it is enough to have all the guarantees one might want (particularly
around multithreading/scoping). Then again, it might be.

 _> 2) How do I make a cycle with Mutex and RWLock?_

The same as a RefCell, just call `.lock().unwrap()` and `.write().unwrap()`
instead of `.borrow_mut()`. These two types are essentially the same as a
RefCell, just with more synchronisation, and fewer panics. They can block and
wait for another thread to finish, at the risk of deadlocks of course, while a
RefCell is restricted to a single thread, so blocking would just dead lock a
thread with itself, hence a panic resolves the situation better. In fact,
other than this blocking/panicking distinction, a RwLock and RefCell are
almost identical: .read == .borrow and .write == .borrow_mut.

Here's a demo which creates cycles that contain a type that prints in the
destructor (demonstrating that they don't run): [https://play.rust-
lang.org/?gist=6ea16b859e22f57fee14&versio...](https://play.rust-
lang.org/?gist=6ea16b859e22f57fee14&version=stable&run=1)

 _> 3) I don't completely understand the queue example, what exactly is the
magic that allows self-reference? _

This is also essentially the same as the core RefCell/Mutex example, replacing
.borrow_mut()/.lock() with .send. An mpmc Queue<T> will be isomorphic to
Arc<Mutex<VecDeque<T>>> (although unlikely to be implemented as such in
practice). The key is that they will reference-count the book-keeping that
needs to be shared among all handles (like the start/tail of the queue), which
allows creating a reference cycle.

------
nickpsecurity
I'm amazed at how well-written and thoughtful these papers are given the
product is a language I oppose. Well, it does have some good ideas. :)

However, what made my jaw drop was section 3.2: "a dynamic model" for making
the system safe and easily implemented in hardware. The section said
Stroustrup deciced not to publish it because it was impractical. One of few
times see his name and go "what an idiot..." What wasn't practical in general
case or that time period doesn't mean it won't be in future in other
circumstances. Good ideas, esp simple solutions, have a way of coming back to
us.

Specifically, efforts like Cambridge's CHERI processor are looking at minimal
modifications to CPU's to greatly increase assurance w/ legacy compatibility.
This concept should've been published as there's use in embedded and appliance
field where a lot uses subsets of C++. He should've published it. I'm saving
the paper for my collection just in case that idea comes in handy.

~~~
pcwalton
> The section said Stroustrup deciced not to publish it because it was
> impractical. One of few times see his name and go "what an idiot..."

He's right though. Such a system would be slower than a good tracing GC, which
completely eliminates these problems and is much easier for the programmer to
use.

~~~
nickpsecurity
In "hardware" as a minimal processor extension as I said? Pointer checks, etc
take almost nothing in HW. Garbage collection schemes have a bigger hit and
more complex integration. Hence, having his published report for details to
add to collection of lightweight schemes out there for C++ or even another
language that might benefit later.

That sort of thing happens more than you know in CompSci. Example: One method
for Oberon and Java appliances I pulled right out of an old Scheme CPU paper
that was also impractical then. Also found a correct-by-construction HW method
in same one. ;)

~~~
pcwalton
Specifically, how do you do use after free detection in hardware without
garbage collection?

~~~
nickpsecurity
I should've clarified that my statement assumes his model works as advertised,
then notes that the pointer checks are efficient in hardware. I couldn't be
sure if he was detecting or preventing use-after-free with what would be in
compiler and what in runtime. Here's the things I was looking at:

"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."

"Every object on the free store (heap, dynamic store) must have exactly one
owner [pointer/reference]"

"set the ownership bit when a value returned from new is assigned"

"use delete whenever a pointer with the ownership bit is set goes out of
scope"

Sounds similar to what I read in Rust guide, esp one-per-location and scope
part. Those and other rules appeared to make it easy for checks to prevent or
catch use-after-free. _If so_ , then my keep it around for hardware suggestion
could lead to acceleration work. If I misread the paper or goals of ownership
model, then my suggestion would've only led to accelerating... whatever safety
it was trying to enforce.

Did that clear it up for you? Or did you spot where I went wrong? Could go
either way given it was a quick read for me.

------
zwieback
I'm glad to see that after all these years Bjarne (and the other authors) have
kept up the tradition of clear and concise writing they are known for.

------
stinos
On one hand it's good things like this are written, on the other hand it's a
bit of a shame it still gets written. We _should_ have moved beyond that by
now, but adoption of modern C++ goes so slow.. Not exactly sure what the
reasons are - sometimes it just seems like stubborness to me - I hear 'legacy'
often, and yes if your codebase doesn't even compile with the latest compilers
than it's a valid argument, but otherwise: it's not exactly hard to interface
modern C++ with C-with-classes. Anyway I've worked in multiple teams and after
the initial stage of getting overit and doing the right thing none of useever
writes code like the 'bad' examples anymore since smart pointers and
collections were made commonly available years ago and they solve most of the
problems in this pdf.

------
cousin_it
A static analyzer for a memory-safe subset of C++? That sounds cool. Has
anyone downloaded it and tried to break it?

~~~
steveklabnik
They have not actually released the tool to the public yet, to my knowledge.

------
jejones3141
Could someone more knowledgeable than me compare and contrast this with Rust?

~~~
Manishearth
Aside from what Steve mentioned:

\- Simpler lifetime system: Both a pro and a con, because some functions will
not be expressible using "c++ borrowing"

\- No `Alias XOR mutability`, which in rust protects from memory issues and
things like iterator invalidation. Not sure if isocpp addresses the segfault
that can be caused by

    
    
        vector<int> v;
        // fill it up with elements
        int *ptr = v[3];
        v.push_back(0); // may reallocate
        cout<<*ptr; // may segfault
    
    

its been a while since I looked at the guidelines and I'm not sure of either
of these points, take them with a grain of salt.

~~~
pcwalton
It does rely on alias xor mutability.

~~~
Manishearth
Huh. I should read it again.

