- If I may just have a quick show of hands.
How many of you, in your heart of hearts,
sometimes are inclined to think of a talk title like that
as a pipe dream?
We feel relaxed and free to be honest.
That was about a quarter of the audience.
I hope to get that down, zero would be too much to ask,
maybe to an eighth by the end.
Let's talk about how we can make C++ evolve in a way
that makes it both more powerful, because we want that.
We don't want to give up anything.
We want to increase the power and control we have
but make using C++ simpler than it is today.
And there is proof that we can do it
because we've already been doing it.
This is how to be intentional along that same path
and double down and do it even better in the future.
So I'm going to talk for most of the talk
on the last two things,
an update on the lifetime profile for simplifying C++
so that we can detect dangling problems,
many common sources of dangling iterators, pointers,
string_views in our code.
Which makes our code simpler
because then we don't have to teach and learn
many of those common things we have to teach today.
And on metaclasses, which makes authoring classes simpler.
But first, before I get into those two,
which are the bulk of the talk,
but they're the what and the how,
let's take a few minutes to talk about the why
because that's much more important
than diving into solutions.
Probably the most common reaction I get,
including a few minutes ago,
about simplifying by adding stuff is,
so let me get this straight.
You want to add stuff to this already huge language
to make it simpler.
I have a coffee I need to get.
But it's not adding things to the language in itself,
it's to make C++ coding simpler, easier to teach,
learn, debug, maintain.
So yes we can do this,
if the thing we add to the language
does one of three things.
And there are others, but here are three main ones.
If it makes user code simpler.
For instance, range for we added to the language,
so we made the language a page bigger.
But we made a lot of C++ code simpler
because they could directly express their intent.
If we can remove special cases and sharp edges.
If we can discourage, maybe even deprecate,
some existing hard features
that make working with C++ difficult,
then we have simplified C++ by adding something.
We do want to also take away.
We'll get to that.
Now let's dig into that first bubble
because there are three major categories of ways
that we can add a feature that makes user code simpler.
The way you always do this,
add something that makes things simpler,
is by adding an abstraction of some sort
because that lets programmers express their intent
The first bucket, which I'll call, say, a K,
or constant factor gain,
is when we add something like range for,
think of it as I'm adding a fixed name into the language.
In that case it's for with a colon.
Sometimes it's actually a word, like override.
But some specific syntax which we can think of as a name,
and then the user can invoke that name
as a built in word of power
and express their intent more directly.
Their code just got simpler
because now you can look at range for,
you look at just that first line,
and without looking at the body
you know it's going to traverse,
visiting each element once, in order,
until an exception is thrown or they get to the end.
By construction, that's simpler.
The second bucket is if you let users make a name,
a word of power.
This is why concepts are so powerful
because we are letting the user write a name, like sortable,
and then use it, and the language helps you
by letting you overload on it
and do other things like that.
And I'll call that an N-fold improvement
because now you've not just given one feature,
you've given users the ability to write small features.
And the third bucket, which is the most powerful thing
you can do in a programming language,
and you have to be careful here
because it's easy to give too much power
in an undisciplined way.
If you need convincing,
recall LISP's defun defun three,
and if you don't know what that means,
google it, and shut it.
What you want to do in the third bucket
is give the user a way to make their own word of power,
make their own name,
that has not just a name but also encapsulated behavior.
It's got a hidden part.
And today we have exactly three such things
in the entire C++ language,
the variable, the function, and the class.
Yeah, templates are great,
but they're just parameterizations of those three.
Exceptions are great,
but they're more in category one.
We have user defined encapsulated abstractions
in the variable, the function, the class.
Now let's start with class, that's the easy one.
The definition for this encapsulated abstraction
is I can give it a name.
Okay, I can give a class a name.
And there's an exposed part and a hidden part.
What's the exposed part of a class?
Yell it out.
What's the hidden part?
And then there's protected,
which means public to a smaller audience,
so it's in between, we're getting granularity.
But there's that separation.
Now, you can go and look at inline functions
in the class body, member variables,
they're visible in the source code.
The compiler uses that for optimization.
The calling code can't use those.
For a function, what is the exposed part?
What's the hidden part?
Again, even if it's inline,
the caller just uses the declaration.
For a variable, what is the exposed part?
Ah yes, the answers are getting slower.
Type and name, exactly right.
And what's the hidden part?
The current value.
And we have accessors
to read and write the current value.
But you can easily write a function that uses a variable
and never looks at its value at all,
for instance, a times two function.
Says variable star equals two.
Never reads it value, it just doubles it.
So it's an abstraction.
Of all the features that are currently in the pipeline
in the actual core group, so in the standard,
or on the way to the standard, in the C++ committee today,
the only other such feature is the modulo.
It is an encapsulation boundary,
and that is what makes it so powerful and so important,
because you can talk about the exported
and the non-exported are hidden
parts of a new user defined abstraction.
And that gives you combinatorial composable power.
I'm gonna talk about two things.
When I talk about lifetime rules
to diagnose common cases of dangling,
that's in the lower left bucket
because we're removing special cases and sharp edges
so we don't have to teach them.
That simplifies the language as it's used.
And metaclasses are in the lower right
because they would be,
they're very early, so an experiment in progress,
but we're about a year or two into it,
and if they progress, if it works out,
they would be another one of those very few things
in the bottom right bucket
that let you incant a new word of power
that's an encapsulated behavior,
and that's very powerful.
In fact, it addresses both of those two other
bubbles on the left.
So pop quiz.
I've shown this slide before
with the title, "Not your father's C++."
On the left is C++ 98 code.
Today is modern C++ 11 code.
There are more modern things since this,
but I've been showing this slide for about eight years,
to the point where C++ 11 feels like a new language,
as Bjarne Stroustrup correctly said.
And we've been doing this all over the place.
On the left is former standard code
that still works, still compiles,
but now we have simpler ways.
How many of you found the trailing return type syntax
jarring the first time you saw it?
How many of you still find it jarring?
A few, a smaller number.
So we learn it.
And soon perhaps we can do
the simplification at the bottom,
maybe as soon as one of the next two meetings.
We already have concepts in the language
and if we have the terse syntax notation
then we can do something like the bottom right.
But here's a serious question.
Of all of the things we just looked at
on this slide and on this slide,
which are C++ and which are not C++?
They're all C++, but they look very different.
All of them are C++ic, we might say,
in the spirit of C++,
even though they look different superficially.
So when we talk about what is C++,
we talk about usually three things.
Two things, but I'm gonna add a third
because it doesn't get mentioned a lot
but it's very important.
The first is, of course, zero overhead abstraction.
Don't pay for what you don't use.
The other half of that, the second line in that bullet,
is important, and often people don't talk about it.
When they say zero overhead abstraction,
some people say, oh,
but your feature costs something if you use it.
That's not zero overhead.
That's not what zero overhead means.
Zero overhead means you don't pay for it
if you don't use it.
But if you do use it, it's gonna cost you something.
You're opting into it.
But it's going to be as efficient
as you could reasonably write by hand.
It's gonna cost something, but it's gonna be as efficient
as you could reasonably write by hand.
That's a zero overhead abstraction.
We also like determinism and control.
And this comes in various flavors,
but I think the right way to summarize it
is determinism and control,
control over time and space.
Bounded execution time, space layout,
knowing where your data lives.
This includes leaving no room for a lower language than C++,
other than assembler.
It means trust the programmer.
All that fits in this bucket.
And then we have great link compatibility
so we can use old C++ and new C++ in the same project,
and link it together, and it works.
A historical strength that has let us
achieve the zero overhead principle and determinism
is that so much in C++ has emphasized static behavior.
Static typing, static compilation,
usually static linking in the standard.
And one of the major current trends
if you look at concepts, constexpr,
and so many of these other things we're doing,
most of them are static,
or enabling us to do things in a static way
that we used to do in a dynamic way.
I'll tell you right now,
as soon as we get static compile time reflection,
I will be leading the charge
to remove typeid from the language
because there ain't no reason we need it,
and we currently pay for what we don't use
with typeid all the time.
And the moment we do that,
our executables will get smaller
by removing a feature almost nobody uses
but replacing that dynamic feature,
which is one reason they don't use it,
with a static feature.
So that's the core, I would say, of C++.
But let's use the bottom half of the slide
to talk about non-core things.
So this is not core, but it's very useful pragmatically.
Backward source compatibility.
Now, when it's backward source compat with C,
it's pretty much read-only.
The idea is we can easily inhale a C header
so we can call the function.
So it's about using existing libraries.
It's not about writing new code.
Remember, Bjarne Stroustrup has been telling people
since the start of this project, C with classes, in 1979,
it's C, yes, that compiles,
with classes, use those instead.
So backward source compatibility
isn't so you can write code that old way forever.
It's so you have a new way to right it
but the backward source compatibility
is there when you need it,
specifically to call existing libraries.
And that's true also for existing C++ code.
We have a great backward compatibility story.
So what is not core to C++?
Well, we already mentioned specific syntax.
We're adding new syntaxes all the time.
As soon as Bjarne added classes,
it was a different language from C, but we liked it,
and the classes fueled the generation of languages.
It's very powerful.
Tedium is not core to C++
no matter how many people believe otherwise.
We can have range for and override, and you know what,
they don't cost anything.
Almost all major modern language features
can be designed with zero overhead.
Like always on garbage collection, no.
Always on metadata, no.
Opt in garbage collection, sure.
Opt in metadata, static reflection,
and you save the ones you want and only those,
totally zero overhead.
So this idea of having a nice language
and having zero overhead is not in tension
nearly all of the time,
and we're improving C++ in exactly that way.
And getting good defaults, removing sharp edges,
like dangling, and maybe your code works,
maybe it will fall over.
That's not core to C++.
If we can solve that with zero overhead,
then we have a better language that is still C++.
So as people propose new language features,
or library features for C++ evolution,
but especially language features,
I would ask you to please consider questions like these.
How does this feature let the programmer
express their intent more directly?
If you can answer that,
you almost certainly have an extraction,
and a good general one.
And then we can tweak about how general,
and how good, and how to make it useful.
How does this simplify C++ code?
C++ as she is spoke, so to say,
to read, write, debug, and maintain the code.
How does it remove gotchas, or special cases,
or sharp edges, foot guns, from the language?
And that simplifies the language
because we don't have to teach them anymore then,
or have them drain our mental overhead
as we're using the language.
Here's an interesting one.
How does this feature remove a place where C++ today
does not meet the zero overhead principle
with its own features, or determinism principle?
There are two such features,
exception handling and RTTI are not zero overhead.
News flash, they're the ones that are commonly turned off.
They're the only two standard C++ features
that every major compiler has a way to turn off.
That tells you people are voting with their feet.
But the reason is because they're not zero overhead
and not deterministic.
If we solve that, which for example we are in fact
already doing for RTTI, for the typeid part,
with static reflection,
because once you've got that you don't need typeid anymore.
You can just statically reflect at compile time
and save the stuff, only the stuff you need, in your binary.
Totally zero overhead.
So how does this proposal help fix
an outstanding problem like that,
which makes C++ even more C++ic than it already is.
How can this feature help us remove something someday,
to potentially even deprecate,
or at least write guidelines and linter tools
that say don't do that and give you errors
in clean new code, like macros.
An example, and I could give many examples
where we've done this.
But a using alias is a good one.
Instead of templatizing typedef,
which I initially proposed because we needed that,
and I didn't think the committee
had appetite for more than that,
imagine how surprised I was
when the response to the paper was, at the next meeting,
paper saying no, let's have a using alias
that is left to right and clean like we would want it,
and templatize that, and just leave typedef alone
for when you need it, for existing code.
I was pleasantly surprised
to see the committee have appetite for that.
And today we are in a world where we can teach people,
and the core guidelines say this,
don't use typedef in new code.
Use a using alias.
It can do everything a typedef could do and more,
but typedef is still here in this bucket.
Now we can have a separate bucket for compatibility mode.
And think of it that way.
Our modern language, and compatibility mode.
And the more we add superseding features on this side,
the more we can move cruft into compatibility mode.
And there's a lot of good we can retain here.
But we end up with a simpler language to teach and learn.
So please think of this explicitly.
We've already been doing it.
But let's start viewing backward source compatibility
as a zero overhead feature in the sense of
today we pay for it in the language and the standard
because we have perfect backward source compat
and we pay for it in complexity of the spec,
whether we need it or not,
and not everybody needs it all the time.
So let's look for ways to keep doing
what we're already doing
and to make new code use the shiny new things,
and distinguish between the latest,
the current modern C++, which is simpler already,
and compatibility mode.
And if we think of this compatibility mode separately,
that will help us to think about C++ is already evolving.
So with that context,
let's talk about the first of the two main topics,
which is the lifetime extensions
for simplifying dangling problems.
So, the goal here is to validate the approach
that I presented here three years ago.
And the approach you're gonna see today,
and you see in the paper that was posted last week
on the core guidelines repo
is still the same model.
The main change that we've done,
besides a few minor tweaks,
is to shift emphasis from whole program guarantees,
perfection, to diagnosing common cases, the 80/90% value.
And we're gonna see examples of that.
Now we actually have two implementations.
The last time I was on this stage
we had a very early, rough prototype,
that wasn't shippable, in Visual C++,
and since then we've gradually shipped more of it.
Today we have two partial implementations in progress,
one based in Clang and one in Visual C++.
So thank you to Neil and to Kyle, who aren't here today.
But Neil was on stage with me three years ago,
and Kyle did a lot of the redoing of that analysis.
And thank you also to Matthias and Gabor,
who we'll see on stage here in just a moment
for their Clang implementation.
One of the goals of this analysis
is I would like to see if we could make it
efficient enough to run during regular compilation,
during regular IDE use.
And I believe we validated that.
This year we can say, yes, that's been validated.
The Visual C++ implementation runs in the IDE
in a static analysis framework extension.
And it runs with real time squiggles.
The performance is sufficient
for real time squiggling of dangling errors in your code.
We'll see a couple of example screenshots.
And then the Clang implementation is totally unoptimized
because these folks have been working
on getting the features in.
Totally unoptimized, but currently,
when building reasonably large LLVM translation units,
it's about 5% compilation overhead.
So, these are rules which we can be confident
are efficient enough because they run locally.
There's no whole program analysis,
there's no data flow analysis, only control flow analysis.
That we can run regularly.
And this is important because as soon as you can say
all major compilers have warning X, you can stop teaching X.
Because when you write coding guidelines,
when you write books,
like Scott, and I, and others have done,
you don't need to write items or guidelines
about things that all compilers warn on.
Today, dangling is not one of those things,
so we have to have whole items in books on these problems.
If we can detect those reliably during compilation
and just expect compilers to do that a few years from now,
we can stop teaching quite a bit of what we teach now.
So let's see an overview of the approach
and then some examples.
The idea is we're gonna look at local flow control paths.
We're gonna enforce it statically.
So again, this is totally compile time.
This is not a sanitizer.
It has zero runtime overhead,
zero impact on your binary image.
The idea is, look,
there's basically three kinds of types in the world.
There's just plain old types that sit there, values.
Then, like a point, int x, y.
Then there are pointers with a capital P
that refer to somebody else's storage but do not own it.
That's a raw pointer or a reference.
It's a string_view, it's a span, it's an iterator,
it's a range, it's filter view for the range's TS.
They're all just that thing.
And we don't worry about the details of all those types.
We just say, there are pointers.
They point to something they don't own.
And the third category is owners.
They somehow store something they do own.
That's string, vector, smart pointers,
any owning containers that you have.
And once we have that, we can have a single set of rules
that knows only about values, pointers, and owners.
It's the only kinds of types it knows about.
And can know when things get invalidated.
Here's a simple example.
By the way, here's the next four slides side by side.
I'm going to show them to you just to show
these are basically all the same example.
Here's a simple one with an int* we set to null.
So what we're going to do
is we're going to record statically,
at any give point in the source code,
as we analyze each function by itself, in isolation,
what does this local pointer point to.
In this case, p, when declared, points to null,
so we say its pset, its points to set, is null.
That's what it points to.
Then we open a scope, we declare an int x.
There's a value.
That's that third category of type.
Then we take the address of x.
The result of the address of x expression is a raw pointer,
which is a capital P pointer.
And its points to set is...
x, I just took the address of x, so it points to x.
When I copy the pointer, and I put it into p,
I just copy its points to set,
so now p points to x, that's its points to set.
And now I can say cout *p and that's fine.
When you do the *p,
I know that, ah, what is p's pset?
Points to x.
That's still in scope, wonderful.
I won't give you an error.
*p is fine.
But now close that brace
and repeat that exact same expression, cout *p,
from line B where it's green, to line D where it's red,
and what happened at that close brace, line C?
x was destroyed, so the rule is,
when the variable goes out of scope,
or the variable is destroyed,
in any psets there are,
all the psets you've got going locally,
any time x is mentioned in one of those psets
replace it with invalid.
So in this case we change the pset of p to invalid.
And now when we try to dereference p we get an error
because pset p contains invalid.
And we can even say exactly which line invalidated it
and what it was pointing to at the time.
And if a pointer can point to more than one thing,
the pset can have more than one entry.
And so you can get false positives
because it's going to be conservative.
But it shows you when a pointer could have been invalidated,
which usually is a bug in your code.
Now this is a simple example.
We can generalize this, as I mentioned,
to user defined pointer types.
And we recognize a lot of them out of the box.
For example, anything that looks like an iterator,
or if it satisfies the range's TS range requirements,
it's just auto detected as a pointer type.
You don't have to annotate it.
There is an annotation,
but you don't need to use it very often,
and if you do, it's you write your own type
that has a different interface,
you can say, I am a pointer,
and then that unlocks all this analysis
for users of your type.
But a string_view, we do that by default.
And you can see the Godbolt example live there
if you'd like to run it.
Please not now, but it's available afterwards.
So the string_view s, when we default construct it, we say,
okay, a default constructed pointer by default is null,
points to null.
Then I enter a scope.
I have a character array.
I say s equals a so I've got now
a string_view to that array.
So pset s is a.
String_view points to array a.
And I can use the string_view s,
but at line C, when a is destroyed,
I go an I replace a in all psets with invalid.
So now p points to,
or s, the string_view s, points to invalid.
And now when I try to use it on the last line,
I get an error.
It's the same expression it was earlier
in the function on line B,
but was valid on line B and it's not valid on line D.
And again, there's no data flow analysis.
We're not tracking values of anything.
All we're doing is following
the control flow paths through the function.
It's a single pass algorithm, very efficient,
and we can compute these answers.
So, just as we generalized to pointers,
let's generalize to owner types.
Same example now, but this time with a string.
A string is an owner.
It owns some data, in this case a character array.
Now when I say s equals name on line A,
we set the points to set of s to name prime.
So if you get a pointer to an owner's storage,
it's to owner prime.
That's just our convention for it.
You're not pointing to owner, which you could also do,
but in this case you're pointing
to the thing owned by owner.
And now there's another rule.
Now you invalidate owner prime
not just if owner is destroyed,
but also if it is used in a non-const way,
if it is modified.
And there are a few cases where you need to annotate,
yeah, this non-const use is actually not invalidating,
and you can do that.
But most of the time that works great,
including that if I say name equals frobozz,
so I'm assigning to string,
I say, ah, any pset that has name prime,
because I modified the owner, set it to invalid
because modifying the owner could,
and in this case, may well have, moved the memory.
And then I can diagnose that that string_view
is dangling on line D but not on like B.
And you can do this in general.
Here's a completely different set
that when one of the implementers first looked at this
they said, oh no, really, C++ does this?
So, some of you have known about this for a lot longer.
Vector, what does this operator subscript return?
A proxy, not a bool ref, because it can't,
because it's supposed to cram them into bits.
That proxy can dangle,
and if you use auto proxy = vb,
your code will work but could dangle if you reassign,
or if you invalidate the vector by push_back, or reserve,
such as on line C.
And we can detect that just fine,
exactly the same rules as before.
Finally, function calls.
Since we're analyzing each function in isolation,
when you call a function we just assume
that any pointers that you get out of it
are derived from its inputs.
I mean, that's what functions almost always do,
is they return stuff that you gave them.
And if it's a pointer,
we assume that it could be a copy
of that pointer that you gave it.
If it's an owner, we assume it could be owner prime,
derived from owner that you gave it.
This can be totally heuristic
because you can override this with annotation.
The whole point of this heuristic
is to make annotation not needed most of the time.
The heuristic is that, hey, you know what?
If you have a const reference parameter,
that combine to R values, people keep forgetting that.
So by default we're going to assume
that any pointer you get back
didn't come from a const reference owner parameter,
unless that's all you've got.
If that's all you've got, okay,
you probably got it from that.
Turns out this finds bugs.
When the implementation returned
a pointer derived from its const ref parameter
and wasn't thinking about,
oh, that could be a temporary.
Or when the caller was doing it
and didn't realize what was going on.
So let's take a look at some examples.
And I have this cute little video
that is showing three phases of dangling.
Now, I want you to see these three phases.
Notice the first is he's dangling,
but everything is okay so far.
There is the dereference. (laughter)
And there's the third stage.
Did you see the undefined behavior?
and then, sometime later, undefined behavior.
Can we say it all together if you'll humor me?
So it's three phases.
That's actually pretty much what happens.
Some time later, that's what you get.
Please welcome to the stage
Matthias Gehre and Gabor Horvath,
who are the implementers of the Clang based implementation.
And we'll look to some examples together.
So thank you very much.
First, thank you for doing the hard work to implement this.
I know you've come a long way from Europe as well,
so thank you for being at CppCon.
And by the way, these gentleman will have a talk
later today on implementing this lifetime profile
we're talking about in this section of this talk,
in Clang, so you can find them there to ask questions
and see a presentation about more details
of the Clang-specific implementation.
But now here's an example which are gonna use screenshots
because conference wifi, and because it's more convenient.
So here's a screenshot from the Visual C++ implementation.
But it's one I know that resonates with you, Matthias.
What's going on here with this min example?
So, we start with declaring two variables, x and y.
They deduce to int, even though they are auto.
And then we use good old std::min
to find the minimum of both.
And we take actually a reference as a return.
So, good will point to y, obviously.
And then we can print good, and everything will be okay.
But, turns out, sometimes later someone comes and says,
oh, that's not actually the business logic anymore.
I don't want to have the minimum of x and y.
I need y + 1, so we just add one.
What can happen?
It's minimum ref.
And it turns out that now
bad still binds to the second argument
because y + 1 turns out to be smaller.
But y + 1 is a temporary,
so that will initially bind to y + 1
but the temporary will be destroyed
at end of full expression, so at end of line 14.
And now, if you do the dereference,
things will happen like we just saw.
- And so this actually uses
those default function call rules I just said.
Turns out std:min needed no annotation
because it takes two references.
People always forget it's reference, not value.
Which is efficient, except when you dangle.
It takes two references and returns a reference.
Well, the references are just pointers.
By default, we assume that the pointer that's returned
is derived from the inputs, which is two pointers,
so we assume it's one of those.
So the points to set you get back for bad
is going to be the points to set of x, comma, temp,
where temp is y + 1,
and then when temp is destroyed, boom.
Does your Clang implementation handle this?
- [Matthias] Exactly.
- Well let's take a look.
So here's the Clang implementation on Godbolt
in the cppx.godbolt.org.
And you can find links to these examples
in the lifetime paper.
There's over 30 links, and here's just a few of them.
And so here we see exactly the same thing.
And I like how you've got two messages here.
- Yes, you will get the warning
on the dereference on line 12,
the squiggles, bad, and it says,
oh, you're referencing a dangling pointer.
Which here's a reference.
It's a generalized pointer.
And it also gives a note
to find out where this reference started dangling,
and that's at the end of line 11.
You see a squiggle under the semicolon,
and the note says, yes, temporary was destroyed here.
Now, Gabor, you had mentioned to me
that you were sitting in one of Jason Turner's...
Is Jason Turner here?
Yell if you're here.
Thank you for providing us examples.
We all can do this.
Here's an example from Jason's talk that you had mentioned.
And so we put it into the compiler.
What's going on here, and what do we diagnosis?
- Right, so, here we have a local variable.
And we will return a reference wrapper,
which refers to this local variable.
And of course, after the function is returned,
the local variable is no longer available,
so this reference wrapper will dangle.
And as Jason pointed out,
what is really interesting about this example,
if we use real pointers or references,
the compiler would issue a warning for that.
But if we use a reference wrapper,
none of the major compilers warns.
And the reason is that the major compiler,
the compiler does not know
what the reference wrapper actually is.
But if we would show the interface
of this class to a developer,
then she could infer that it's something like a pointer.
That is its type role.
And this is exactly what this analysis does.
It can infer that this is likely to be
something that acts as a pointer,
so this helps us detecting this issue.
- So this particular one,
we never tested reference wrapper,
but because it matches the pointer heuristics
we didn't require annotation,
and it just recognized it out of the box,
and it happened to get it.
So, thank you.
Now here's another one that was also in Jason's talk,
but it was also in an article in a standard paper
this summer at our last standards meeting
by Nico Josuttis and Richard Smith of Google.
So, everybody comes across this kind of example.
So we had it in a talk, in a paper.
What's going on here?
- Maybe you heard a lot of things
about how string_view makes the language more unsafe.
So what is happening here, we have a string literal
that we are converting into a std:string.
And during this conversion we will create a temporary.
And std:string will convert into string_view
because there is an implicit conversion.
And this string_view will refer to this temporary,
which will be gone by the end of the full expression.
So in the next line, the string_view will then go.
So, even though this behavior
is kind of dangerous in this language,
but if we have the right tools
it will not cause any headache.
- So interestingly, do you notice the error
is on line seven, not on line six.
So remember our animation.
It wasn't just a gratuitous animation
to see somebody smack their head.
Like, there actually was a teaching value there
because dangling is one thing.
It's dereferencing that's where you hit your head.
And then some time after that you'll get the actual bug
manifest as undefined behaviors.
The swing comes back and conks you.
Line six is perfectly fine.
In fact, we want to be able to write dangling pointers
as long as we don't dereference them
because you do that every time
you reuse a pointer variable in a loop.
And you set it on the next trip through the loop
to something else, and then you use it.
Then it dangles, but that's okay.
You don't use it until your next trip through the loop.
You want to reuse variables like that.
I actually don't like the choice
certain other languages have made
that diagnose as soon as you invalidate
because invalidating a pointer is not a bug.
It prohibits reusing variables.
It's dereferencing or using an invalidated pointer
that's a bug.
So I really like that the error is one line seven,
not on line six.
But now this next example, Matthias.
This example, first of all,
thank you to the 30 people who replied
to the SCM challenge puzzle
where I asked you to come up with examples.
We're gonna have three winners in a moment.
A quarter of you gave variants of this example.
Most of those said vector erase.
How bad is this bug, and what's going on here?
- So, let's first look at what's going on.
We have a vector declared in f.
Fill it with some elements.
We do an iteration over the vector.
And when we find an interesting element, say two,
we say, oh, that shouldn't be here, so we erase it.
But of course that makes our vector change size.
And so if you don't update our iterator, do something to i,
then I will not be what we expect it to be.
In the case we see on the screen
we will not even find that in testing
because I will just point to the next element.
We will skip one, which we didn't intend,
but it will kind of run fine.
But then in production we might actually
remove the last element,
and then, by skipping and incrementing the iterator
on the next iteration,
you'll actually skip past the end,
and then we will start looping over uninitialized memory.
So this is an error in all cases.
And the analysis finds this
because it has a notion of where i points to.
i points into v.
And then we call a non-const function on v, which is erase,
then all iterators that pointed to v get invalidated
because you should get a new one.
- One very interesting thing about this example.
There's two lines mentioned, six and eight.
Which one is the error?
- So, the error happens on line six, where the warning is,
because we do increment some iterator.
But the dangling happens on line eight.
And that seems to be wrong order
because usually you first dangle and you dereference.
But because the loop, I mean,
that's how we observe it.
In one iteration, where you see the two, you erase.
And then in the next iteration
we're incrementing the iterator.
Then you do some operation to an iterator which is dangling.
And that's why you see, logically,
the dangling happens after the dereference.
- And if you look in the paper
you'll see how this is described.
Basically we take a one pass through the function.
If there's an if else,
those are basically just divergent,
and then fork and join control flows
in the control flow graph.
For a loop we treat it just like an if else
except we do it twice.
Once for the first iteration,
and once for every non-first iteration.
Only once, not end, just two iterations.
The first, and then one to all the others,
to catch any invalidations in the body of the loop
which would affect subsequent iterations.
So a first and a non-first.
And now we teach people to write it the bottom way.
You have this as an interview question, don't you?
- Yes, and it works pretty well.
So, for reference, you can see on the lower part
how you can do it to avoid dangling.
And that will also work in the checker.
So it makes the checker happy and the interviewer
because we will use the iterator that's returned from erase.
And even though erase itself invalidated the vector,
we know that the function will return something valid.
I mean, we don't assume that functions return invalid state.
We will check that in the function body,
but here will assume it had call side
and that's the correct thing to do.
So the fresh iterator we get out of erase
will be perfectly valid.
And in fact, one reason you have this
as an interview question
is because Scott Myers has basically an item on this
in Effective STL, so it tests whether people read the book
and know that.
So, sad news is, now we don't need that item anymore
if every major compiler gave this warning
because you would just want it all the time.
You might need just a very shorter item saying,
when you get this warning, write this pattern.
But you wouldn't need to explain the whole thing
or how to watch out for it.
But now let's talk about null.
And Gabor, you had some examples that interested you.
What's going on in f1?
- So, if you look at the first function,
the interface doesn't tell us a lot.
For example, we cannot know whether p might be null or not.
So in this case we assume that this value might be null.
If that's not the case maybe it is better to use references
or some kind of annotation.
- And I like your note on line one saying,
here are the things you could write this as
to silence the warning, which actually improved the code.
If you look at the second function,
you will see if we insert a null check in the function
we will no longer warn for the potential null dereference.
This not only makes the code safer but also clearer
because this way we can see that the user
expects p to be null in some cases.
And if you look at the third example
you can see that we do not constrain you
how to write this null check.
Because in both of the cases we have two branches.
On one branch, the pointer is null.
On the other, it is not null.
So it's that easy.
And if you look at the next function, it's a bit trickier.
We still detect that there is an assert
which is basically a pre-condition check in the current C++
so we will recognize that
and we will not warn on the next line.
But there is a caveat to this
because if you are compiling your code in a release mode,
the offset might disappear because it is a macro.
- It doesn't just disappear from their code.
It disappears from the control flow graph
that Clang gives you so you can't see it anymore.
So, unfortunately, really as built,
you might see a warning there.
Fortunately, in the upcoming standard,
we will hopefully have a solution to that with contracts.
- Yes, which were just voted in in June,
just three, four months ago, in Rapperswil.
So we're very glad to have that.
And then you can write line 17 as,
learn to love square brackets,
attribute assert colon p,
or write it as an expects pre-condition on f4.
But now, what about function f5?
- In function f5 we have a null check
but the null check is not correct.
Even if p is not null but q is null,
we can still...
So, if p is null and q is not null,
we can still dereference a null pointer.
And this check will see this.
And in order to fix this error
we need to check the guarding condition.
We need to fix the guarding condition.
- And finally, what about this pointer chasing
node function on line 33?
- Yes, in the last function,
we can see that when we have a loop
basically we have branches.
And we can handle this in a very similar way.
So, after the loop, we either exited the loop
or didn't take it in the first place.
And in both cases, the n must be null,
so we will be able to find the null dereference.
- And this is actually a hard, unknown,
difficult problem in static analysis theory.
And one of the reasons is
because if you do data flow analysis,
you do something more than local control flow analysis,
then you have to start dealing with this loop,
and who knows how far it goes, because it's dynamic.
You can only do so much statically.
There is no data flow analysis going on here.
We're simply saying, look,
there's a fork in the control flow graph, and then a join,
and along one of those it's going to be null,
or it can still be null.
In this case it's going to be null.
So thank you.
Now the next, the last three examples,
are taken from that Rapperswil paper by Josuttis and Smith
on all the evils of dangling.
this lifetime profile catches all those errors.
Are, or almost all,
are caught by the Clang implementation today
that were in that Rapperswil paper.
And here's another one from there.
This is the Visual C++ screenshot
just so we show that implementation as well.
What's going on here, Matthias?
- So, we have a function, findOrDefault,
which probably many of us have written
in one or another way
and had to wish that it was in the standard.
And it takes a map and a key,
looks up the key in the map,
and if the key is not there it returns a default value
and that we have a last parameter.
And because we don't intend to mutate the map,
or the key, or the default value,
everything can be const, and be, of course, references,
because we don't have to have copies.
So that's how we went to our design.
And then we call it.
So we have our example function.
We declare map.
It's empty, so we will not find the key at all.
We have a key, which is obviously not in the map,
and then we can call findOrDefault.
And as a default value with some string will none,
so we hope that s, at the end, will point to none.
But what actually happens is
back in the previous example,
none will be constructing a temporary string.
And that will go into the function.
And the function will return the temporary string,
so s will bind to the temporary that contains none.
But then the temporary
will be destroyed at end of expression
and we have a dangling string.
So what we do here is
we don't actually need to know
how the implementation of findOrDefault works
because that would need like inter-program analysis,
and we don't do that.
We just look at the argument types and return types,
and have a heuristic that tells us
what will the return type probably point at.
And what our heuristic tells us in this case,
that the returned thing, so what s binds to,
is key or defvalue.
It's one of those because I have matching types.
And when we now do the analysis locally in the call
we see s will either point to key,
which is a local variable,
or to this temporary that was passed,
that got constructed from none.
And because it is one of those,
and the temporary leaves scope, s gets invalidated,
and we will see the error when we dereference it.
- Excellent, thank you.
For time we'll skip over the next couple of examples.
You'll find many more in the paper.
But we also want to acknowledge
three examples that were submitted in the SCM challenge.
So, these three folks, if you're in the room,
can you please stand up?
Can we see you, and can you come to the front?
Hayun Ezra Chung, Stephane Guy, and Isabella Muerte.
All right, run, run.
It's such a long room.
Give them a hand. (applause)
Thank you for this example.
Here is the summary where we're calling a function,
and even though we're going through
all sorts of callback forwarding interfaces
with references and generic lambdas,
the analysis is simple, it sees through all that,
and points out that in line 12
we're getting a pointer to a temporary back
and gives a nice answer.
So thank you very much for that.
Our second answer was Stephane.
Is it "Gee" or "Guy"? - "Gee."
- I was right the first time, thank you.
And this is from Titus Winter's talk,
so thank you for giving an example from another talk
earlier at this same conference this week.
Again showing, in this case a directory name
that takes a string_view, returns a string_view.
We see through all that, we see the temporary,
and diagnose the dangling.
And then Isabella, this was a classic
and very famous example, a variation of it.
That made it a unique one, so I enjoyed that.
Which is actually just tweeted by John Regere recently
and also discussed by Brian Kernighan.
And again, notice that you could think
in lines 13 through 15,
hey, this is just returning a pointer to a local.
Yes, except that current implementations
don't see it that way.
This analysis does, and we get a nice error.
So thank you to all three of these.
Thank you, also, to the people who assisted with our demo
for remember, danging, there's the dereference,
and then the undefined behavior.
And thank you to Matthias and Gabor.
Let's give them all a hand.
Thank you very much.
And you can see their talk later today.
Remember this checklist when proposing a new C++ feature,
what are the things we should ask?
Well, I think, I hope that the lifetime profile
addresses two of those things pretty directly.
How does it simplify C++ code
to make it easier to read, write, debug, maintain.
Well, this analysis gets rid of
a whole item in Effective STL,
and it gets rid of classes of errors
that if we just could assume that people didn't have it
on by default in their compilers as warnings,
we wouldn't need to teach or document.
We would at most need to teach
what you write instead, when it's not obvious.
And by removing gotchas, special cases, classes of errors,
even though it's not trying to get all dangling errors,
but common classes of errors
that we get over and over again,
in articles, talks, at this conference,
in code, in standards committee mailings,
and just diagnose them,
we'll have so much more time to talk about better stuff,
and move the language forward,
and to deliver solid code.
So for the rest of this talk let's talk about metaclasses.
And this is a different kind of simplification.
We're gonna switch gears
and talk about simplifying class authoring.
And I first introduced that last year.
I talked about it here at CppCon.
And I want to thank Andrew Sutton.
Andrew, are you around?
Please stand up and wave.
Who did the implementation in Clang
together with Jennifer and Wyatt.
I'd like to thank Jennifer and Wyatt too.
They're not in the room today,
but we thank them very much.
Also, is Matt Godbolt in the room?
Where are you, Matt?
Matt, keep standing.
Don't we all want to thank Matt Godbolt?
And in particular, for hosting this implementation for us
at cppx.godbolt.org, the metaclasses implementation,
for the past year plus.
So thank you.
So, one of our goals over the past year.
This is a progress report.
This is a long lead item.
It's still an experiment and effort that could fail.
I sure hope it doesn't
because I see a lot of promise.
And here's the progress so far.
It has gotten in SG7 for several reviews in the committee,
in the study group, SG7, on compile time programming,
and reflection and generation.
And I'll show you the feedback on the next slide.
It was largely syntactic
but it helped us think about what we were doing better.
But the structure is still the same.
And the prototype now handles more of the examples.
Like last year it handled
about four of the examples in the paper.
Now it handles more, and I'll show a few.
But first, in a nutshell, remember the idea is,
the C++ class, this big apple,
already is flexible enough to express
iterators, traits, properties, flag enums,
base classes, values, irregular type, variants.
And they're all expressed with
this wonderfully powerful class concept
that is so powerful and flexible it can express all that.
The trouble is because we can't give a name
to a subset of that universe of classes.
We can't give a name to an iterator.
It helps us detect them, it doesn't help us author them.
We don't get as much support for writing them,
making sure we wrote them correctly,
and so it tends to be brittle.
There tends to be tedious boilerplate.
We need to remember rules
because the compiler doesn't enforce them.
We need to write functions that aren't generated.
The idea of metaclasses is that many language features,
not all language features
that we could imagine adding to C++,
but the ones that are about kinds of classes,
those future language features
I hope that we never need to standardize
because I would like to be able to write them as libraries.
That is the very high goal that we're going for here.
And with no loss of usability, expressiveness,
diagnostic quality, or performance,
compared even to other languages
who wired them right into their language.
And I gave some demos last year to support that.
If you think that's a high bar
please watch the demos from last year.
But I'm gonna give you a quick update
and then show you new examples.
When you're writing a metaclass,
the left hand side was last year's syntax,
the right hand side is this year's syntax.
And the main difference is in the wrapping.
So, the body is still the same.
So the concept is still the same, the structure.
But instead of the idea of,
oh, I'm sort of mentally inside the class I'm writing,
and I'm reading my own definitions and modifying them,
now the view is, no, I take a fully formed class,
whatever the user wrote in source code as input,
and I generate, wherever I am, things from that.
And that could be I copy each item.
That could be I do some validations.
But the main thing is that
you'll notice at the bottom of the right hand side
that we might now generate,
explicitly generate things that were implicit before.
I think this is an improvement.
But the structure is still the same,
but that's the new syntax to get used to.
And they'll probably tweak again,
but that was the committee's syntax feedback,
and I appreciated it.
The other one was when using a metaclass
to define your own type.
So on the left hand side was last year.
The right hand side is this year.
The committee really, really wanted
to see the keyword class up front.
And there are parsing benefits to that.
We can still parse the other one
but there are definitely parsing benefits.
And also, by having interface in parens
you can have a comma list and have more things there.
It becomes easier to express multiple metaclasses.
But that's the change to applying the metaclass.
So now let's see some examples.
In each case I'm going to show the example
the way I teach it today.
Every single one of these is examples I already teach,
and you might have been in a course where I have taught,
so I will very quickly give you a condensed version
of what it is we're trying to accomplish,
how we do it today,
and then we'll look at how metaclasses help us.
Associating data with locks.
How many of you have ever written a race condition?
So, we forget to take a lock,
or we took a lock, but oh, it was the wrong one.
That one passes code review more often.
So here is the pattern.
And I'm gonna blow through this fairly quickly.
But the main thing is to get this sense
of what we're asking people to do.
If we grouped the mutex at the bottom
and the data it protects
together in the same class or struct,
now we could get instrumentation to figure out,
oh, I now know that mutex goes with that data.
I can do things like assert the mutex is held
before I access the data.
Write an accessor around the data
and assert the mutex is held.
The middle three lines are boilerplate
because they following the locking concept,
or the mutex concept in the standard library,
just like the container concept is
you have begin, end, value_type.
The mutex concept is you have lock, try_lock, unlock.
So we write those too
just so that the user can then take a MyData object
and do a lock_guard on it directly.
So this works fine.
But man, that's a lot of boilerplate to write by hand.
Now, if I was gonna automate this,
the first thing I need to do,
notice I assert the mutex is held.
Turns out the standard mutex doesn't have that function.
So we can wrap.
If we want to make a mutex testable,
basically what you do is
you put a mutex and a thread id together
and then you can provide your lock, try_lock, unlock.
Every time you lock you record the thread that held it.
And when you unlock you reset it to the default.
And then you can say is_held.
So this is a helper I'm going to use.
It's a little wrapper you write
if your mutex doesn't already have an is_held function.
And then I always have to apologize for the next slide.
I apologize for very few things in C++
because I love this language.
But what's the thing we always have to apologize for?
Starts with M.
So, I always start with, sorry for the macros, but...
And yes, there are ways you could write this
in a different way that could avoid the macros,
but I don't believe you can write it conveniently
without macros in this way
because I want to write the code at bottom.
I want to write here's my struct MyData.
It's guarded with mutex type.
It has a guarded member vector,
a type vector named v, and so forth.
And in that guarded member I have to refer to...
First of all, I have to use token pasting
to make a different member name for the private member
than for the accessor,
otherwise I've got to have the user,
at the bottom, specify two names
just so that one is the public name
and one is the private name, which is icky.
But also, inside the macro I have to then refer to
the mutex that I declared earlier.
So this is what we do today.
It's what I've been teaching.
After the class, half the time somebody comes up and says,
here's how I think you could write this without the macro,
but it's a different design that's also a decent design,
but it's not this one.
I really would like not to write the macro.
But let me motivate why it's still worth it today.
And by the way, we're never likely to standardize
a special purpose language feature here in the standard,
so this is today's status quo.
Here's why it's good.
Now imagine I have a MyData object,
two of them, data1, data2.
When I call data1.v.push_back,
as soon as I call data1.v,
what's the very first thing that happens
in the body of that accessor function?
Assert mutex is held.
Here I didn't lock it, so I'm getting a green line
because it will fail.
That's green because failure is good.
That's exactly what I wanted.
At test time, the first time I ever exercise
that line of code in a unit test,
I will get an assertion.
That is way better than a random,
hard to reproduce bug report from a customer site.
The very first time I go through that first green line
at test time I will get the assertion.
Same thing on data2.w.
First thing it does is is that mutex held, data2's mutex.
Answer is no, I assert.
Great, I just caught two race conditions.
So let's say I comment those lines out,
and I go into the block,
and now I have that nice yellow lock_guard line.
Notice I'm using a lock_guard on my data
because it has lock, try_lock, unlock.
Now I say data1.v.
The very first thing it does is...
Is data1's mutex held?
The answer is true, off we go,
and the push_back is perfectly fine.
Then I say data2.w, the very first thing it asks is...
Is data2's mutex held.
Now I have taken a lock but not on that mutex.
That assertion will correctly fire
and again that green line will get an assertion,
deterministically, at test time,
the first time I exercise that code.
And I cannot say how much better that is
than a timing dependent race.
The one thing you shouldn't do
is take that sneaky pointer while you've got the lock,
access it, store a pointer to it,
release the lock, and then use the pointer.
I can't detect that.
Fire that person. (laughter)
But I can detect Murphy.
Maybe not Machiavelli, but I can detect Murphy.
So this is a valuable idiom,
but it makes me cry because
it's so valuable I teach macros.
I show it even though I need macros to teach it,
which is embarrassing.
So on the left hand side is what we write
either by hand, or at the bottom with macros.
With the metaclasses proposal you can wrap all this up.
And there's a live Godbolt example
that I'm going to post in the paper
when it gets updated for the next mailing.
And I'll also show these examples on my blog
over the coming few weeks and months.
But I'll show you the code today.
It's a class.
It's guarded with mutex_type.
And I just say what the members are.
I use natural syntax, not like the macros,
which are macros plus they don't let us use natural syntax,
and I just generate what's on the left hand side.
I can directly express intent
and that makes their code simpler
because they can utter a word of power.
And we've enabled them, with metaclasses,
to write the word of power guarded.
By the way, did you notice this is a templatized metaclass.
So today, a metaclass is defined
as template type T, T source.
But here there's an additional template parameter, M,
which is the mutex_type.
So what I'm going to do...
and I'm showing part of the code.
There's a couple of constexpr functions it calls
that do similar things up above,
and you'll see that in the online example.
I first call line 51, guarded_with.
And what that does it generates
lock, try_lock, unlock, and the mutex of type M.
Then for every member variable
I do a guarded_member of its type and its name.
That is not a macro anymore.
Rather, guarded_member creates an accessor
and a private variable with a suffix name,
which I can do without macros now, with generation.
And I create one of those for every member.
That creates the accessors.
And you see them on the right hand side
where I create the accessor for v, and for w,
and the private members.
And I even have a compiler diagnostic
which at compile time, if somebody tries to make
a guarded class that has no data members,
I can give them a high quality diagnostic.
Because why did you guard something that's empty?
And then I use it in line 70 at the bottom.
All I write is class(guarded<:mutex>).
The same code I just showed you compiles to that today.
And I didn't need a language feature to do it.
Reflection and generation is powerful.
What's even more powerful is the metaclass
being able to give a name to a bundle of those things
and reuse them.
And so here's what we use.
So I would say if you hate macros,
you have a good reason to love metaclasses,
and I hope you do.
And if we're serious about...
Whether people happen to like metaclasses
immediately today or not,
if you're serious about getting rid
of the remaining reasonable use of macros, and I am,
then you're going to love modules,
you're going to love constexpr,
and you're going to love reflection and generation at least.
I happen to think that you will already, if not soon,
also love metaclasses to be able to do this kind of thing
because now I don't need to teach that macro anymore
if we have a standard way of doing this.
And you might have noticed
that today when we talk about patterns
we talk about cookbooks that you apply by hand.
People have long said, yeah, patterns,
you can't make those to be libraries.
Oh yes you can.
Let's talk about another pattern and make it a library.
First, let me give you the motivation,
how we teach it today.
Threads are a low level way of doing concurrency.
It's the only way in the standard really right now.
But threads are too lower level.
There are no guard rails.
They can do just anything in their execution.
And their communication is with shared state by default,
and that's just terrible.
So we teach discipline your threads.
When you write a thread...
This isn't for every use case,
but it's for the vast majority of them,
write your thread mainline as a message pump
that just accepts asynchronous messages
and does them one at a time,
which means it needs no other synchronization
on its internal shared state
because it's only pumping, executing one message at a time.
And then communicate by sending it async messages.
And make them well formatted.
Hint, we'd love to use the type system,
such as, say, a function signature.
That's statically a well formatted message.
A member function signature
is a statically well formatted, type safe message.
And there are various options here.
I'm just gonna show a very basic option.
You could do this with coroutines.
You could use a series of tasks on a thread pool.
You can use a priority queue, multiple channels.
I'm just gonna show an actual thread with one queue.
And I'm pretty sure that most people here seeing that
can then write the advanced examples pretty quickly
But let's see how to do the basic example.
And understand that in C++
we were never going to standardize
something as narrow as an active async class.
I don't see that ever happening.
Like, somebody proposes a class
where every member function call is asynchronous
and automatically returns a future?
It's useful but it's too narrow a feature.
This isn't the language we would do that in.
We like general features.
So we encapsulate a thread in the message queue.
We want to write code like this
where we have a class worker that's coded specially somehow,
and when, in the bottom, we invoke it, a function,
we actually get a future,
and then we can do other work concurrently
while we wait on it.
So every member function call is asynchronous
and returns immediately.
We want to directly express
all these different kinds of things.
Long running workers like a GUI thread,
decoupled independent work
like we'll see pipeline stages in a moment.
So here is the key thing we want to do.
We want to attach thread lifetime to object lifetime.
Because C++ knows all about object lifetimes.
The way we do it is we teach it the way C++ knows it,
which is hook the constructor and the destructor.
So the constructor starts the thread and the message pump.
The destructor sends a done signal
and then waits for the queue to drain, and then joins.
You'll see in a moment how that directly lets us express
object lifetimes in a clean way in real code.
Here's a starter example.
Once I've done that, I can have a nested class inside,
so two active classes, one nested inside the other.
And then when I instantiate the outer one,
and when I destroy it, I naturally, by construction,
wait for the inner one to destroy first
because I automatically destroy it.
The language gives me that, thank you, Bjarne.
It sends the done message
for the internal active thread too,
waits for it to drain its queue, then returns,
and then I do the destructor for the enclosing object,
which sends the done signal for it.
And so I've expressed nested concurrency really simply
using existing concepts in the language.
And this is really powerful.
So how do I write this pattern,
or make it easy for people to write it?
Here's a helper class.
So, a message is just a function
that takes and returns void, just something executable.
My constructor starts the message queue.
And it starts the message pump.
While (!done) mq.receive()(), get the next message,
and then notice, paren paren, immediately invoke it.
There's two sets of parens there.
That's not a typo.
And that's it, that's the message pump.
The destructor sends done equals true as a message
so when that message is executed on that thread
it sets done equals true,
and then on its next trip through the loop it says,
oh look, done is true, and exits.
So we're sending the message that will cause the thread,
the worker thread, the message pump, to finish,
in order, once it's drained all previous messages first.
Then we just join and wait for that to happen.
And Send is just a wrapper for enqueuing a message.
So that's the basic model.
Now let's say we use that to, on the right hand side,
write a logger class, that's thread safe.
It's safe to call externally
without external synchronization.
Whereas on the left hand side,
the old way we would do it would be with a mutex.
And we take a lock inside of every call
to the println function.
So the calling code is identical.
The calling code is mylog.println("hello %1 name").
What's the difference between those two?
Just shout out any differences you see
in how they work.
Left one is synchronous, yes.
So what's the right one?
Which is more scalable?
Right hand one.
Well, because on the right hand one,
does the caller ever block?
No, caller never blocks.
Now what are we doing?
We're trading off space for time.
We're gonna queue up messages,
so we're gonna use space in order to get that concurrency.
But the right hand side is non-blocking,
and so it will scale better.
And the left hand side works fine
but it also does all the work on the caller's thread.
The other nice thing about the right hand side
is all the work is done inside
the Active object worker thread.
So not only is the caller not blocked,
they're also pushing more work off their thread
because the actual formatting in the right hand side
is done on the worker thread.
And so then you need to load balance this
and make sure you're doing
the right thing for throttling and everything.
But in general there's a lot to like
about the right hand side.
And we can write this today.
And notice the pattern.
You make Active your last member,
and then you write every member function,
you wrap its body in an a.Send lambda.
That's what we teach people today
as the convenient way to write this.
Now let's say that we have a typical pipeline.
So here's an example I give in class.
And basically what they have to do
is they have to create a decorate thread,
a compress thread, and an encrypt thread.
The decorator decorates a buffer,
puts it in a queue for the compressor,
which takes it, compresses it, and so forth.
So there's three pipeline stages,
each a thread connected by queues.
And so people take about a half an hour
and they come up with this solution.
Maybe 20 minutes.
Then I say, now write it generically using Active objects,
and then I show them this answer.
You just write a Stage.
So here's a generic Stage.
It's just going to take a function,
some function that takes a Buffer*
and does something with it.
I don't know what it does.
Just something that can take a Buffer* and stores it.
Then it has a Process function,
which notice follows this asynchronous pattern.
And this is the message.
It's an asynchronous message
that does whatever that work is on this Buffer.
And then the private state is just it stored its Buffer,
it stored its work function that it's supposed to do,
and an Active object at the end.
Remember to put that one last.
If you don't put it last,
then the active thread's mainline
starts before the function is constructed.
You really, really want to put that Active object last.
Always remember that with this pattern by hand.
As soon as I have this,
I construct it with a piece of work to do,
I have an async process message
that just does it asynchronously
and pumps the process message for everything,
every Buffer pointer that it's given.
Now I can write the whole solution in a few lines like this.
It's very clean.
I create a stage named encryptor,
and I give it a Process function which simply says
for any Buffer you're given, just encrypt it.
That's it, I'm done.
Pretty much no boilerplate there.
That's pretty much the exact statement
of what the encryptor does.
The compressor takes a Buffer pointer, it compresses it,
and then sends it to the encryptor,
which it captured by reference.
And that's fine because these are nested lifetimes.
There's no dangling possible here.
And notice, by construction,
I had to construct the objects in the right order.
I had to create the encryptor first
because the compressor refers to it.
I can't write the code the wrong way
because the compressor refers to the encryptor,
so of course I write it first.
Then I do the same with the decorator.
The decorator is a stage which, for each Buffer,
decorates it and hands it to the compressor.
And then I simply call the decorator
and send it the Process message,
and fill up its queue.
And I've got this three stage pipeline that runs.
And the cool part is, at the end, what do I destroy first?
What does the destructor of decorator do?
It sends, it enqueues a done message for the decorator,
waits for the queue to drain.
So any decorations it was doing it waits for it to finish.
Then it sees the done message, joins and returns.
Decorator is done.
Then it destroys compressor, same thing.
Sends it the done message, waits for the queue to drain.
We're draining the pipeline in the right order naturally
because reverse order of construction
is what Bjarne saw was the right thing
and has given us for 39 years.
Thank you, Bjarne.
And then we join, and then we destroy the encryptor.
We send it the done message, wait for its queue to drain,
join, and return.
And I have managed three worker thread lifetimes.
I've done orderly setup and teardown.
I could not write it the wrong way
to get it in the wrong order.
Everything is clear by default.
I love C++.
If you wrote this in Java, you can do it,
but you're likely going to write the bug at the bottom.
Because you still have to setup the stages in order
because the encryptor has to refer to...
The compressor has to refer to the encryptor.
But you typically copy code
and then destroy them in the same order, which is wrong.
You have to destroy them in the opposite order.
Java fixed this in 2014
with the try-with-resources statement
which lets you approximate what C++ does
with nested local lifetimes.
But just compare what we write in C++.
This is just so clean.
And I've managed three thread lifetimes
and set up the multi-stage queue and drained it cleanly.
And I can easily add flexibility.
What if I wanted to add another end stage, archiver,
and the compressor sends some things to the encryptor,
some things to the archiver.
It's easy, it's like three more lines of code.
Very general, very clean, and correct by construction.
The only glitch is that today I have to write it by hand.
With an active metaclass I can just say class(active) Stage
and I can automatically every non-constructor non-destructor
be an asynchronous function.
Now, for Process it returns void
so it's actually pretty easy.
But at least I already see a benefit here
where I don't have to declare the active member.
I don't have to remember to declare it last.
I don't have to wrap every body in a member function.
And with the logger, same thing.
We saw the logging example.
I can write that as an active class.
But how do I do this?
Well, let's walk through it briefly.
Async says, first of all, echo all the data members.
For every data member there is, generate it.
Because whatever the user wrote, that's their data.
Then for every member function, remember its return type.
If it's not a constructor and it's not a destructor
we're gonna want to make it asynchronous,
because constructors and destructors are still synchronous.
So first thing we do is we make
the one that the user wrote private.
Then we generate a wrapper that, notice at the bottom,
it creates a promise, extracts a future,
and calls the original.
And does all that for you.
And then we generate the actual function itself.
And then I can write at the bottom, class(async) Test.
And there's my one function, h.
And if we go back up
you'll see all the stuff that was generated,
including the wrapper that takes the promise.
And that's the whole code so far.
And that also illustrates how hard this is by hand.
Because what I usually don't teach
right up front in the talk,
when I teach the Active object method today I do teach it,
but I leave it and the end and say,
yeah, but what if you have to return a value?
Okay, let's talk about promises and futures,
and how to use std::promise, and here's how you do it.
So you actually have to write
all of the stuff on the left today for this pattern
if you want to actually return something.
With the metaclass I just showed you you write class(active)
and I'll just take that, and that double GetResult
just automatically returns a future.
And anybody who calls it, auto whatever = GetResult()
gets a future.
It's just much cleaner and correct by destruction,
by construction, ha ha.
And by the way, we've been talking about
this member data of active types.
That's really thread local storage
except with none of the overhead.
It's by construction thread local
but it's just normal allocation.
I kind of like that.
Yeah, joining threads for the win.
Thread local storage by construction for the win.
Don't need special language features.
How many of you use properties in C++?
Yeah, a few hands.
Even though they're not standard
you are likely using a compiler that supports them.
It's been invented many times, in Qt, in Microsoft, Borland,
even in Clang because it has a Microsoft mode,
and it also has support for other languages inside Clang
that do have properties.
And they're useful in practice and generally liked.
But every time they've been proposed
for standardization they failed
because it's a narrow language feature.
And the reality is, this committee is just never
going to standardize property as a language feature.
There's just too much debate about,
eh, is it desirable,
eh, our language is already big enough,
do we really need that language feature,
does it carry its weight.
Which are reasonable things to ask.
The reality is we're just
never gonna get it in the language.
But what if we could write class-like language features
as libraries if we have generation and metaclasses?
Well today in C# I can write this.
Pardon for the C#, but it's just an example
of another language with properties.
This is sort of a default version of a property
where if you just write get and set forward declared,
it generates a data member of that type
and a trivial get, return that member, set, set that member,
accessors for you because it's so common.
And you can, with the implementation I'm about to show you,
write this today.
Very close to that syntax.
I have to put one extra word in,
but I think I can get rid of that next week.
Thank you, Andrew.
You can write class property of string,
empty braces, so I want all the defaults,
and I want to call it Name.
I'll show you how that works in a moment.
But if in C# you want to, or in Qt,
you want to have a non-trivial property,
you want to actually say what your data type is
because maybe it's a compound data type,
and you want to write your own get and set function,
you can still do that.
We just won't generate the defaults for you
if you actually wrote them.
So that's the model that we want to achieve.
Here's how we do it.
So the property metaclass, the very first thing it does
is we're gonna say, okay,
I need to remember whether there's a value member
because I'm going to use that later.
First thing in my for loop
I'm going to go through all of the member variables here
and I'm going to say, is their name value?
If it is, I'll set hasValue equals true.
And then I'm going to generate that.
So I'm going to echo whatever the data members are
because there might be more than one
if they wrote their own data members.
Then for every member function I'm going to say,
was it get or was it set,
because I'm now going to use that
to give you them if you didn't write them yourself.
So I'll remember that.
But mostly I'm just going to generate that myself.
So I'm going to echo the functions that you wrote,
if you wrote them.
Now, in the case, if there are no data members
and there's no get function,
then I'm going to generate a value of that type.
So this handles the empty property case.
But this is just compile time code, right.
With reflection and generation I can write this.
If I reflect and I see there's no value,
I will generate a value.
And I'll remember hasValue equals true.
So far I have echoed any functions and data they've got.
I've generated a value member if there wasn't one.
Now I'm going to say, if there's no get function...
Every property has to have a get function, right.
Const properties need it.
Non-const may not have it, but const need it,
otherwise why have the property.
Now I'm gonna say, if there is a value member,
either generated, or the one that I just created,
or the one the user wrote,
I'm going to now generate my get accessor.
If the user didn't write one themselves
this is the trivial one they get.
And I'm also gonna give a nice high quality compiler error
if they didn't write a default member, a value member,
they wrote something else,
and expected us to write the getter.
You can't do that.
You have to either give us a value or write your getter.
You can't leave both to us.
We'll do both...
Well, we'll do both of them,
but you can't just have some members but not a value
and expect us to know how to write the getter.
So that gives us a nice high quality error right there.
Now what about the setter?
Not every property is writable.
Well, in C++ we have const,
and we have in C++ already, std::is_const.
So I'll just use that here.
Instead it's const_v because I like C++ 17
and I hate writing ::value.
So if T is const,
and we don't already have a setter,
and we have a value member, member named value,
then I'll generate a set function.
And just because I can,
I'll overload it on const ref and ref refs
for it to be maximally efficient for our values.
Because you're not gonna see this code anyway.
Let's do the efficient thing.
It's only one more line.
Then, finally, I provide a conversion operator
that returns the value of this type.
And then I'm going to generate
the ways to set this value.
Because what I'm going to do is I've created this class,
and I'm just gonna make one value of this class, that's it.
Link time code gen is really good at,
and link time optimization, and combat folding
is really good at removing duplicates.
So I'm just gonna generate a class for this property
and instantiate one variable for all time from it.
And these are how you construct and access it.
And then finally, today I have to write this and this.
This code works today.
I really would like to write this
but I'm so close to it already I can smell it.
And I know this one is gonna be going away soon.
And the committee wants class brackets, so,
okay, we'll give them class bracket.
That's close enough for me.
But that works today.
And I'll be blogging about it in the next few weeks
with a live example you can try.
So the goal of metaclass is to expand
the abstraction vocabulary we have in C++.
And beyond just the built in metaclasses, you might say,
of class, struct, union, enum.
We can generate defaults, and force requirements on classes,
and give a name as a word of power
to this group of behavior, this kind of class,
that has these characteristics
out of the universe of classes.
And get rid of lots of side compilers I hope,
such as Qt moc, COM IDL, C++/CX, which I designed,
and part of my goal here
is so I never have to design that again.
Because I want to write it in C++.
We did C++ CI and CX
because we couldn't write the information in C++.
There wasn't enough there.
I believe this gets us the rest of the way
so I can write it in a potentially future,
if this goes well, standard C++, somewhere down the road.
If we can do this,
then you would not have to wait for the standards committee
to agree to add property to the language.
You could write it as a short library and share it.
You could actually do it.
We could add it to the standard library.
That's way easier than plumbing it through the language,
which would take man centuries of work, probably.
And who has time for person centuries of work
per language feature.
The benefits for standardization
is if we can get more work into short, clear libraries
which are testable, than as language features,
which have to be wired into a language specification,
and aren't testable easily.
You have to pay somebody a lot of money to write test cases.
I like code, code is great.
Code is testable.
And being able to write that feature property
as code that I can test is just a wonderful thing for me.
So here's the checklist of things I said earlier.
I think it hits all of them.
It doesn't quite let us remove help exceptions and RTTI,
but it does pretty much all the others, including the last.
Because as I showed with the guarded case,
metaclasses and generation remove
some of the reasons we need macros today.
This is a direct step on
putting the nail in the coffin of macros in new code
by systematically, in this and with other features,
removing the reasons that remain to write them.
So we talked about more powerful and simpler,
we talked about lifetime,
and we talked about metaclasses.
And it's time for XKCD.
Randall wrote it, I didn't.
The reason I mention this
is because how many of you noticed
that the lifetime rules compiler
and the metaclasses compiler were both in cppx.godbolt.com?
Oh, only a few of you noticed that.
Let's have a couple of slides of fun.
Here is one more simple metaclass.
A metaclass called pointer.
And it's just a toy metaclass
but it still does something useful.
Every time you write a pointer-like type,
whether it's an observer pointer, or an iterator,
what do we have to teach people?
Oh yeah, overload operator star and operator arrow
and make them do the same thing.
Well here's a little metaclass
where you just write operator star.
We make sure you wrote it
so you can't make the mistake of forgetting it.
And we just generate operator arrow for you.
So what it does is we're going to say,
we're going to echo all the variables that it might have.
We're going to just check whether it has an operator star.
If it doesn't we're going to complain.
And then we're gonna generate operator arrow
that returns address of star, star this.
Just calls the star operator.
And once we do that,
we can simply write our own pointer type.
We write only operator star,
and if we forget, we get an error.
And we get operator arrow for free.
Okay, so there's a cute little toy metaclass
but this still does something useful.
And here's what it generates.
I wrote my data members, my constructor, those get echoed.
I wrote my operator star, that gets validated.
And then I generate the arrow operator,
and everything is fine.
And then when I use it I can create one
that's default constructed, and I can dereference it.
And you might have noticed it has a squiggle.
What do you suppose that squiggle means?
If I take away the blackout,
you will see the first part of the compiler output
is the generation of the pointer class using the metaclass.
And the second then says,
you're using that pointer type
and you're doing a null deref.
Set it to something before you dereference it.
And I have one piece of code,
and I get both of those together.
I like features that work well together.
And this is an example of how these simplifications
add up and combine well.
So let's try to pursue C++ evolution
that makes user code simpler through abstraction.
Remove special cases and sharp edges.
And let's start to get rid of things,
to deprecate or at least discourage older features
that are less good, like macros.
And lifetime targets that I've talked about
targets the lower left.
Metaclasses target the other two.
And hopefully these are ways that,
with these and other features,
we can continue making C++ a simpler language for us all
in the coming years.
And I think that would just be great for the industry,
for all of us, and for everyone.
And we'd have a lot more fun in the language too
that we already enjoy so much.
We have time for just a couple of questions.
If you'd like to come up to a mic I'll take one or two,
and then be happy to talk with you down at the bottom.
- Hi, thank you.
This metaclasses seems to be a really powerful feature,
but how are we supposed to debug and test them?
- Excellent question, how do you debug and test them.
So I talked about that last year,
so definitely watch the longer answer in that video.
This is one of the reasons I show that compiler.debug,
the reflect on the metaclass,
so you can see what's outputted.
That is the kind of information, what is generated,
that tools would expose so that you would be able
to step into and do things like that,
and see what you're generating.
But it lets us write that a lot more cleanly.
Is there somebody at this mic?
Okay, the next one here.
- So, in the new syntax for metaclasses
you're accepting a source parameter,
which from all the code examples you've shown
appears to be non-mutable.
How do you control where you're generating to?
In some of the examples I'm guessing
you're generating internal structs and things.
But can you generate outside of the class
and how do you control where that goes?
- So currently generation is where you are in place.
So as you're defining class active, say,
you're in that scope.
You're defining the contents of that class.
One of the things that we're extending
is being able to write non-member functions.
So today you can write member functions,
you can write static member functions,
and free functions that are declared in the class.
And we're also providing ways to be able to declare things
in enclosing namespaces and places like that.
So that's coming.
I don't have any examples that compile yet,
but that will be coming in the coming months.
Oh yes, please.
- So how would this mesh with, say, concepts?
Can you say, like, oh, I have my pointer metaclass.
I would expect this algorithm to have the concept
is a pointer metaclass.
- So, one way to think about it,
and I'm using Bjarne's words here.
- [Attendee] Pointer is probably a bad example.
- But that's okay, iterator, anything like that.
So metaclasses are for constructing,
for defining new types.
Concepts are for querying them, so they're read-only.
Now, every metaclass also comes with .is
so you can say whether it matches, just like a concept.
So, think of them as constructive concepts.
That's the term Bjarne came up with,
and I think it's apropos.
Because you're not just reading something.
You're defining it to be this kind of thing
so you get generated defaults and things like that.
The one thing that concepts does
that this doesn't, and that's useful,
and we could add the feature,
or we could just use concepts here,
is concepts, whereas here you iterate
through the member functions and say,
does it have foo, does it have bar,
concepts you can say, is foo of x a valid expression.
You can do a use case based query.
And that's still useful,
so I think these are very complementary features.
You'll find some examples in the paper
that use concepts in the implementations of metaclasses
to query, oh, is this type assignable.
If it's not, I'll generate something.
It can just check for assignment or equality comparison
use concepts' use case style syntax
and then generate using the syntax you saw.
So I think they go well together.
This is a feature interesting feature.
- I'm very excited by the lifetime checking features.
And I'm curious to know that
if you have sufficient annotation,
is it possible that we can say mathematically
that all dangling dereferences will be caught?
And if so, what was the motivation
for keeping this in tooling
rather than as part of the language itself?
- Well, so, there's a couple of questions there.
So first of all,
this is designed to be implemented in the compiler.
The Clang implementation is in a compiler.
So it runs at compile time.
It's a compile time diagnostic.
So, that's the kind of thing that can be in the language
if we wanted to standardize something like that.
So it can totally do that.
As for whole program guarantees,
it's possible that we could approach those,
or even get them.
I'm a big fan of the 80/20 rule.
I'm a big fan of, the dangling problems I see in C++
are not because it's not formally type safe and memory safe.
It's because I have regular patterns
that people trip over all the time.
And I can get those.
Then we can see, well, how much is the other 20%,
or 2%, more likely, worth.
It's worth noting that
to get a formal guarantee of anything,
it's a cost benefit.
We're taking a cost, like a design time cost,
possibly run time instrumentation,
to get a guarantee.
How valuable is that guarantee compared to the cost?
So for example, type safe languages like Java and C#
aren't type safe.
But it depends on your definition of type safety.
They implement the type safety they specify.
They don't detect use after dispose, for example.
They have resurrection instead,
and get to teach that.
So it all depends on how you define it.
I believe you can get a very practical level
of leak freedom, and dangle freedom, rather,
by being able to handle many common cases
that we already know are real cases
because we've been teaching people about them
for 10, 20 years.
So I'm optimistic that we will be able
to handle the important ones.
Then we'll see if we get to any whole program guarantees.
Yeah, one more please.
- So, at the beginning of the talk
you talked about features in the language
that basically have been
completely overtaken by newer features,
so are there for compatibility.
Like OpenGL expresses a core versus compatibility profile.
What are your quick thoughts
on expressing the standard in such a way?
- That is exactly what I had in mind
and tried to recommend we start thinking about
in the first part of this talk.
I think we are already effectively doing that in practice.
In particular, the whole reason Bjarne
created the core guidelines,
Bjarne and I created the core guidelines,
was to be able to create a set of rules
where you don't do this in new code.
That's for old code only.
New code, we set up guard rails and rules for
here's the subset of C++ that is recommended today,
and that we can keep moving forward.
So, that is implicitly creating such a distinction,
so we're already partway down that road.
I am specifically including a call to action
to start seriously thinking about a future, someday,
where we actually have here are compatibility mode features
with a fence around them
that we only need when we're in compatibility mode,
and consider having a mode
that is a subset that is new code only
that doesn't include that.
- [Attendee] That all compilers would agree on what that is.
I'm certainly speaking only for myself.
I am not speaking for Bjarne.
I am not speaking for the standards community.
I am just observing we're already doing some of this.
I happen to think it's desirable
to start thinking about
is it worth standardizing such a subset.
We'll see where it goes.
I think it's gonna be some years off,
but it's worth thinking about now in order to plan for that.
So thank you.
Thank you all for coming.
Enjoy your lunch and the rest of the conference.