Herb Sutter: Hey have you enjoyed the week
so far? I'm not sure if both my ears are working
right. This half of the room, have you enjoyed
cppcon so far? You, guys, what do you think?
I don't know. They might have had an extra
decibel. Let's hear all of you one more time.
Thank you.
This has been such an awesome week for C++
and also a really festival atmosphere here.
We want to thank you all very much for coming
and it's been a blast. We're now at our final
plenary session. I'd like to introduce the
speaker whose name happens to also be Herb.
Please, give a warm welcome.
Hi. My name is Herb. You're supposed to say,
"Hi, Herb."
Audience: Hi, Herb.
Herb Sutter: It has been 1 year, 3 months
and 18 days since my last template Meta program.
Today, I want to talk about the importance
of resisting complexity. I never thought it
would happen to me. I was introduced to complexity
by a friend or at least I thought he was a
friend. He showed me a cool programming trick
on his computer at home in private and it
really worked on his compiler. I started using
a little complexity, but just a little bit
and only at home, but after a while as I got
used to using complexity and something strange
happened that the thrill faded. It didn't
seem as complex anymore. It seemed simple,
even though other people around me told me
it was still too complex.
I started hanging out with people after work
for social complexity on bar napkins, but
then it started eating into work time that
we started bringing it into the office and
checking it in. Some of you were at the template
metaprogramming hackathon last night and here's
a picture.
Seriously, the reason that I want to talk
about complexity is people who are under the
influence of complexity often think that they're
thinking more lucidly and really that's not
true, but we get so used to complexity that
we should remember that it's fine in moderation.
Once you know a lot about anything including
C++, it's really hard to forget that you're
an expert. There's roughly give or take, it's
hard to get number people who include, use
C++ on their day jobs is roughly around three
million for some time and it's gradually,
it was pretty flat in the 2000s. It's increasing
a bit now, but compare that to many of the
experts that you've heard of.
How many of you use new GNU, GCC? Lots. Great.
You're using the standard library implementation
libstdc++, which has been around for quite
a while and just over the last almost 20 years
I asked some of the maintainers, "How many
people have contributed to this?" I said,
"Well-known open source project everybody
knows that everybody contributes to open source
projects, how many people really have contributed
to it say more than just a few lines like
have contributed something significant to
it?" Answer seems to be about 30 in the world,
in the last roughly 20 years.
How many of you use Clang? If you're using
Xcode that's also Clang so put up your hand.
Okay. You know how many people or developers
of this standard library that ships with Clang,
libc++? The answer until 12 months ago was
2. They were both here at this week. One is
now getting on a plane, the other still here
in the audience. The answer now is about five
to seven who have contributed more say just
a couple of lines, have contributed something
significant.
I was not able to get a number of boost developers
that's been going on for almost 20 years now.
I figure somewhere in the order of say 300,
maybe that's high, I don't know, but let's
be generous. I saw WG21 meeting attenders
who like ever attended more than one C++ meeting,
I didn't actually go through all the minutes
and count, but I'd be surprised if it was
more than 300 people.
Here is a Venn diagram of all the C++ developers
in the world and the smaller circle is all
of the libstdc++ developers, libstdc++ developers,
boost developers and anyone who's attended
more than one ISO C++ meeting, and if I had
put a border, a line around that circle, you
couldn't see the smaller circle. I actually
had to turn off the line around the circle
otherwise you couldn't see the smaller line.
By the way, can you see it?
We need to give good guidance to the big circle,
while still teaching the advanced tools for
people who need advance code including very
advanced code, but, A, not everyone needs
to know all of it and not everyone who knows
it needs to use it all the time. That's a
theme of this talk. In fact, if you go back
to Bjarne Stroustrup's wonderful keynote on
Tuesday, interesting factoid. If you go through
slides know how many double ampersands rvalue
references are in this entire talk? Nice round
number. The value of simple usable defaults
is really priceless.
By the way, I mentioned this on the panel
on Monday night, the most important C++ book
you should go out and buy it at the bookstore
now is A Tour of C++ by Bjarne Stroustrup.
We have lot … I know … I saw … You stole
my thunder. People are yelling, "It's sold
out." I know so, but the good news is that
you can still go to the bookstore and they
will order it and ship it to you because they
do have free shipping, so go to the bookstore
anyway or Amazon or anywhere. In particular,
this book is important because it encapsulates
in one place what every C++ programmer should
be expected to know.
Now, that's really important. If somebody's
coming new to C++ or their learning modern
C++ after you used C++98 they often ask, "Well,
okay, where, what's all the stuff you need
to know?" "Well, look over here on Stack Overflow
and Scott's got a book and to be honest [inaudible
00:06:33] longer book and there's some chapters
in there you ought to know." Here is a 180
-pages that some of you will have time to
read on your plane home this weekend from
cover to cover that tells you the stuff that
everyone needs to know about C++ will recommend
this book. Let people know. Help get the word
out. It's really important. I'm very glad
that Bjarne has made that available.
In this talk, I want to focus on defaults,
basic styles, and idioms. Now some experts
have pushed back at me on this. When I say
the word default some experts treat it as
a bad word. They say, "You're dumbing down
the language." No. I'm not. A default is not
dumbing down especially a default does not
mean, "Oh, just do this blindly and don't
think about it." That's not what it's about.
You should know your other options too, but
the default is about … Unless you have a
good reason to do something else, which does
involve thinking, don't overthink it. Just
do this, unless you have a reason to do something
more complex. In particular, this goes right
in line with the general good advice including
these three items at the bottom of the screen,
which are the titles of three of the first
eight or nine items in the C++ coding standards,
but not coincidentally right for clarity and
correctness first, avoid premature optimization.
In particular, prefer clear code over optimal
code.
Now, that's almost swearing to a C++ audience.
Like, "We should be optimal by default." No,
you shouldn't. You should have good performance
by default. If anyone tries to tell you that
C++ has been about writing optimal code by
default, correct them gently because it has
not. C++ has always been about writing efficient
code not perfectly efficient or optimally
efficient, but good efficiency code by default
and always being able to open the hood and
take control over memory layouts, over the
actual, even down to the instructions that
get done, when you need to, you don't always
need to open the hood.
This talk is not about don't think, it's about,
let's not overthink because isn't it true?
We revel in complexity. How many of you, we're
not going to put a camera on you, and none
of your friends here will tell you. How many
of you would say, "Yeah, reveling in complexity
has described to me at least point in my coding
career?" I see all the speakers have their
hands up. That's good. This is good. We're
acknowledging our problem then we can start
to deal with it. We overcome denial, step
one.
Let's talk about range-based for loops. We
can overthink range-based for loops. I think
that everything I want to say about range-based
for loops pretty much fits on one simple slide,
and here it is. Why do this when you can do
this? Any questions? Now, some of you may
be saying, "Oh, shouldn't I be writing autorefref
there?" Maybe, but wait for it, we'll talk
a bit more about refref at the end, but this
is perfectly good. In fact, no matter whether
C is constant or not this does the right thing
because autoref also because it goes through
the iterators. If you get a concentrator back
that references to a const, it will deduce
the right thing. It'll be read-only for a
read-only collection. This is simple code.
If you are traversing every element of a collection
prefer to write range for.
Now, if you need early break, range for doesn't
support that yet, but ranges look like, there's
a range proposal coming for the November meeting
that I think will address some of that and
even enable that. Certainly, if you're writing
a for loop that reaches every element of the
collection just write range for with autoref
in case you want to modify in place. Soon,
thanks to Stefan [inaudible 00:10:25]. Stefan,
are you here? Wave if you're here. Meow.
Okay. If I see one more code example checked
in with kitty, I think I will scream. For
e:c is going to be a shorthand that the Standards
Committee is seriously considering, and therefore
some compilers, once this committee will bless
it, hopefully, at the next meeting for C++17,
will already start implementing. You don't
have to wait three years to start using this
that basically says, "You know what? We're
going to declare a variable e for you and
it will be of type deduce whatever so the
constants will flow through.
The nice thing about this is the first thing
you might think is, "But how can C++ do that?
Isn't that a breaking change?" How many of
you suspect that the last line may be a breaking
change that can change the meaning of valid
existing C++14 code today? Oh, you're all
scared.
I thought it was, and then, Stefan kept reminding
me, "No. No. You can't actually write that
today. You can't use an existing variable,
you always have to declare a new one." We'll
just drop the type. The default, this sensible
default will be essentially the same as before,
autorefref or autoref, which will do the right
thing. This does not mean that you can't lift
the hood and write it yourself if you want
a certain type or even write the naked iterator
based for loop if you want to do something
more like break partway through or skip elements.
That's all fine, but defaults matter.
You are often influencers of people in your
companies, of people who ask questions on
Stack Overflow, who participate in the community
so please, you teach starting from the bottom
of the screen up, not the other way around.
Now, let's talk about smart pointers. Use
smart pointers effectively, but I still want
you to use lots and lots and lots of raw pointers
and references. They're great. Yeah. Stunned
silence. We hate pointers and references,
all pointers and references. Well, why would
you recommend those? Of course, you should
write those. Those should be your default
parameter types and return types still.
Let's talk about that. Now, clearly, the code
on the left-hand side of the screen is stuff
that was perfectly good code in books some
time ago, but we tell people don't write that
anymore. In fact, basically, all the red stuff
shouldn't pass check in anymore in new code.
Don't use owning pointers. Don't use explicit
new. Don't use delete except if you have a
real performance need, encapsulated down deep
inside load some low-level data structure,
maybe, but FYI, I believe even the entire
STL can be implemented with no loss of performance
using unique pointer. I'm not using new and
delete.
We're about to try that, and so, that exception
maybe even a much smaller set than you might
think because unique pointer is roughly free.
It is very razor thin overhead, often none
at all over a raw pointer. Modern C++ says
use unique pointer, use a shared pointer,
use make_unique. If you want to write new
to allocate a new object, use make_unique
by default.
Now, if you know, again, it's a default. If
you know the object is going to be shared
definitely use make_shared by default. If
you don't know, you're creating an object
and handing it out. You don't know if it's
going to be shared, start with make_unique
because you can always move that into a shared
pointer that then can be used as a shared
pointer group, but if you know it's going
to be shared, use make_shared.
For delete, don't write anything. This is
advice we're giving people. How many of you
follow that advice already in your code basis
today, at least, for new code? Looks like
about half. Okay. How many of you have tried
to follow this advice and still found some
dangling pointer problems with reference counted
smart pointers? A few. I have a slide for
you. I know what the problem is. There is
a cure. Wait a few slides.
A very important qualifier on this. This slide
says don't use owning raw pointers and references.
What does that mean? Really, a pointer that
you should be calling delete [inaudible 00:14:46]
at some point. Non-owning pointers and references
are awesome, keep writing them especially
for parameters and return values. When you
talk about structured lifetimes where one
function calls another, and that callee executes
entirely within the scope of the caller because
then you return and resume the caller. Anything
that the calling function is keeping alive
stays alive for as long as the called function.
Now, there is a reentrancy case. We'll talk
about that, but generally, this is nice. It's
Russian dolls all the way down where the lifetimes
nest perfectly. You don't need ownership transfer
down to call stack unless you're going to
take something out of the call stack. You
don't need to talk about ownership.
In C++98 classic we would say, "Hey, if you're,
if you need to look at a widget and it say,
'It's a required parameter,'" and I'm ignoring
const. We'll come to that later. Pass it by
reference or if it's optional, pass it by
pointer. Are you ready for the modern C++
advice? It's the same. It's the same advice.
Now, if you have a unique pointer, a shared
pointer then, you want to dereference it or
call get to pass a raw reference or a raw
pointer, but raw references raw pointers for
the win. They still are the preferred parameter
type. Think about it this way, if you're not
convinced about that and you feel uncomfortable
think about it this way, unless you're actually
talking about ownership transfer, in which
case it is perfectly valid to pass smart pointers
and I'll show that in just a second.
Unless you, the called function actually is
going to participate in the ownership somehow,
if you just wants to look at a widget, why
on earth does the callee care if you're managing
that lifetime by a unique pointer, shared
pointer, whether it's global, whether it's
on the stack? He should be agnostic to that,
and this is how to write that, very simple,
and the classic advice is still true.
But antipatterns hurt pain pain as STL would
likely say, "He's got me in the habit of talking
like this." It's viral. The antipattern here
is just routinely passing a reference counted
pointer even by reference. This is not just
about shared pointer it's about [com 00:17:00]
pointers or any kind of reference counted
pointers, the ones in boost as well or passing
it by value, which is even worse if you don't
really intend to keep a copy after you return,
to participate in the ownership somehow because
if you go back to that, because what's the
performance of the lower-left code?
You're going to do an increment decrement
on every call, which isn't the end of the
world, but, A, it is an atomic operation so
there's synchronization, and it's not that
cheap, and second, this is premature pessimization.
Premature pessimization is when you have two
options that are equally complex, neither
is really simpler than the other. Well, you
should take the faster one. This is opting
for something that is actually a little more
difficult and slower. If all you need to do
is look at the widget, pass a reference, or
raw reference.
A related antipattern besides passing function
parameters is in loops. When people will frequently
make copies of smart pointers and loops and
that's even worse. We don't want to do those
things. For example, Andrei, when I was discussing
this with him some months ago he mentioned
that in late 2013, Facebook had an experience
with this where their product rocksdb, which
was passing shared-ptr by value quite a bit.
Change those to pass by raw pointer and raw
reference, and observed a 4x speed improvement
from 100k to 400k queries per second in one
of their benchmarks, and other improvements
across the board. This stuff matters. Before
asking for needless work, we'll pay for needless
work.
I wrote a [inaudible 00:18:38] the week about
this recently. Reference counted smart pointers
as well as unique pointer. Any owning pointer
are about managing and owned objects lifetime.
Only use it as a parameter type if you actually
want to talk about the lifetime, you want
to participate in it. The callee is going
to keep a copy of it to put in some global
registry or something so he's going to participate,
keeping it alive. This applies to any reference
counted smart pointer, but we have shared
pointer so we should know about that.
How do we pass smart pointers? If you are
writing a factory that produces a new object
especially an object that's polymorphic so
you're not going to return it by value if
it's polymorphic in particular then prefer
to return a unique pointer by default. I will
show if you know if it's going to be shared,
prefer returning a shared pointer and using
make_shared, that's in the second half of
the slide, but a default, a good default is
return a unique pointer. Why?
Because if you don't know that something more
is needed like shared pointer, anybody who
gets that can do whatever they want with it
if they ignore the return type, it doesn't
fall off the floor. The return value doesn't
fall off the floor, it gets correctly cleaned
up, although, why are they asking the factory
if they're not going to look at it, but whatever,
maybe they're passing it as an rvalue to something
else that is a temporary expression and cleanup
just happens. If they want a shared pointer
they can move it into that. If they have their
own juicy custom shared pointer that they
are using in their shop that's non-standard
and gloriously customized they can call that
get and move it into that.
This is what we should be returning from factories.
Similarly, if we're accepting, our function
is accepting ownership of the object passed
by unique pointer by value that shows that
we're consuming the widget, you can't call
that with a unique pointer in the caller without
saying std move. That helps you see I'm transferring
ownership in. We'll come back to that one.
If I want to reseat, notice I'm not, it's
not about changing the widgets. It's about
if I want to reseats the unique pointer itself
to point to a different object. Well, that's
a typical in/out parameter take that by reference.
I have no idea why you would ever legitimately
want to write const unique pointer reference,
why would you want to simply just look at
it? There's nothing useful I know of that
you can do to a unique pointer except get
the widget in which case you should have just
returned, you just taken widget ref. As far
as I know, writing that as a parameter type
is a think [inaudible 00:21:03].
Now, as I mentioned if you know that the object
is going to be shared that a factory returns
definitely return a shared pointer, and you're
going to use make_shared inside because that
gives you a nice optimization. It's one memory
location instead of two and some better locality
that you'll keep benefiting from. If you're
going to have a function that takes a copy
of the shared_pointer, pass the shared_pointer
by value.
Notice this is saying I am going to keep a
copy either I'm going to put it in some other
data structure or do something else with it,
and this will retain a reference count. If
I want to reseat a shared pointer that is
make it point to something else, pass it by
reference that's as if it will or it might
receipt the shared pointer, and finally, if
you're in the shared case but it's not unconditional.
Sometimes you're going to keep a copy sometimes
you won't then take const ref, and then, if
you take a copy then copy inside if you do
actually need a copy that way you're not going
to incur the cost of the reference count bump
in the cases where you don't actually keep
a copy so that's if you conditionally keep
a copy if you might share.
How many of you are thinking, "Hey, wait a
minute for sync, shouldn't that be unique
pointer refref?" I'm curious how many people
are thinking that? Yes. I have slides for
you later. How to do it right?
This is partial advice. Never pass smart pointers.
By value prefer, unless you actually mean
to traffic in ownership. Prefer raw pointers
and references. They're still great. Express
ownership with a unique pointer by default,
but shared pointer if you're going to share
and use make_shared in that case. Now, when
I post these slides will be available after
the conference. I put this yellow Post-it
to obscure it on top so when you print it
out you can't print out this version. I want
you to print out the version two slides from
now where I'm going to add one more line.
I promise to tell you the pitfall with reference
counter shared pointers. This is something
that if you've been using reference counted
pointers for some time you've probably fallen
into. I know that most teams of any significant
size, in software of any significant complexity
that especially that go over a boundary, and
then, get callbacks into themselves they sometimes
find they drop a reference count like, "What's
going on?"
This is not unique to shared pointer, it happens
with com pointers, it happens with any other
reference counted pointer you might use. If
you're an objective-c developer, it will happen
with those. They're slightly different with
retain and release semantics and so forth
an arch, but this principle applies in general.
Let's talk about the one case because I believe
as a community we now know exactly how to
simply articulate it and to simply solve it.
First here's the problem. Take a second to
absorb that code, and think about what would
be a problematic caller. We have some shared
pointer that's static or global, non-heap
or it's aliased on the stack, non-stack or
aliased on the stack, g_p. It's a shared pointer
to a widget.
Now, I've got a function f that takes a widget
by reference then call some other function
that uses the widget. Function f looks fine
to me. Oh, by the way, function g happens
to among other things receipt the global pointer.
Maybe it's got a reference to it that's passed
through another parameter, maybe it's some
globally visible thing on the heap that it
can get access to. One way or another it receipts
it, and now, in your code, if you happen to
call f of star g_p because you're following
the advice I just gave you to just pass raw
references, right?
You're following the advice I gave you, and
now, what will happen? Bad stuff. Why? What's
wrong with this code? Shout it out. [crosstalk
00:24:57] The assignment to g_p might release
the last reference count on the smart pointer,
it might not, might pass your unit tests and,
but if it does release the last reference
count then in f, when I call g, I'm suddenly
actually blowing away my w object. This is
not obvious, and no, but that's laugh worthy,
but that's not obvious. That is so not obvious.
Then, I use w and it might or might not be
there anymore. Might be the stored memory
might even be gone. The antipattern, so what
is wrong in the code? Like who do you blame?
Don't dereference a non-local possibly aliased
shared pointer. That should no longer pass
code review. I'll show you in a second what
I mean and what to write instead.
Pin it using an unaliased local copy. You
want a shared pointer that's … You can dereference
a shared pointer safely without this re-entrance
problem if it's on the stack and nobody else
has a pointer or reference to the shared pointer.
You haven't effectively made [inaudible 00:26:04]
passing out an alias to your stack based shared
pointer.
In the code at the bottom, you're perfectly
immune to this. Why? Because you say, "Before
I enter my call tree, which could be hundreds
of functions deep, just once, at the top,
where I go from knowing that this object is
owned by a shared pointer to passing it in
at the very top for the first time, converting
it to a reference. Only there take my extra
add ref, take my extra increment once, and
then, for the whole tree.
You can statically test for this and do lint-like
testing for this to make sure that you only
dereference local unaliased reference counted
pointers, and if you do that, you will eliminate
that entire class of problems and it will
only cost you the extra add ref or the extra
increment exactly once, exactly where you
need it and it's an easy pattern to follow.
By the way, when you dereference it you might
also get a pointer and one easy way to do
that is to call a member function, do the
same thing realized there you are also converting
to a raw pointer and follow the same advice.
Just in case, the member function is re-entrant
somehow and might change the location of the
object itself that you're executing the member
function in. That one happens less often but
follow this simple advice, this one simple
trick. It's the modern way. I won't make it
link bait. I'll tell you the answer and you
won't get the problem on the left-hand side
of the screen. Now, here's the summary of
how to do it right and I'll add just that
one green line. Remember take the unaliased
and local copy tree call tree and don't pass
the direct reference, direct dereference of
a heap-based one.
That's reference counting. Write make unique
by default, make shared when you need to instead
of new and delete. Don't use owning pointers
or references, but remember that non-owning
pointers references are ideal on function
parameters and return values because they're
stack. They're structured case, the scoped
case where they're simply observing and they're
wonderfully efficient and good for you just
as they always have been.
The only time you actually want to copy or
assign or pass a reference counted point or
any smart pointer is if you actually want
to deal in the lifetime. If you actually want
to modify the lifetime semantics do it there.
It's funny how people are afraid of four little
letters. How many of you have had one or more
water-cooler conversations, arguments, debates
about auto?
Yeah. I'm going to tell you the truth and
you can tell everybody else that they're wrong.
Are we good with that? Yeah. Like we're all
… Like even we're all going to agree, but
let me offer some guidance. Let me justify
why I'm offering the guidance, and then, it's
up to you, we're all grown-ups to decide whether
you'll follow it yourself, but it's guidance
that I think is simple, works well, saves
from pitfalls. Therefore, it makes sure code
simpler so it's a good default to use, and
is part of a broader shift in the syntax and
style of modern C++ that as it continues to
move to a regular simpler style.
Now, our first plenary speaker, Nigel Tufnel.
That wasn't one of the options and, oh, it
should have been. I was looking … I went
to the Scott Myers [inaudible 00:29:40] site
because I wanted to see if you could put a
write-in response, but, no, you couldn't.
Nigel Tufnell also known as Christopher Guest,
who has played that character on Spinal Tap.
It's okay. It's really simple. Auto is not
that hard. In fact, if you get Scott's book,
which is almost out, there are two items saying
roughly what I'm about to say. Some of the
details are different saying roughly what
I'm about to say about using auto.
Here's the spoiler in one slide. When you're
declaring a local variable, this is all about
when you're declaring a local variable. If
you want the type to track, and I'll give
you reasons why you often do, simply deduce
it and the easiest way to do that is to have
the type of that local variable be auto. Yes,
there will be times when you want to have
the type stick, and that's perfectly good.
The advice is not to use, just deduce everywhere,
the advice is to use auto pretty much everywhere
because auto does not always mean deduce.
You can as the second last line shows, use
auto and still explicitly ask for a type.
That it makes it concrete. Now, you might
say, "Well, why would I do that instead of
just the last line?" That's a style point,
but I hope to convince you there are at least
some reasons to consider even from stylistic
grounds, but also on correctness grounds the
second last line, but they're both good. If
you want to make a type track, which makes
your code more robust and maintainable deduce
it, you'd be guaranteed don't get conversions
and other advantages to make a type stick
do say it, but you can still use auto.
Consider this code. What does this code do?
What would you name the function? Anybody
want to throw out any function names? Say
again. Add another. Hey, that's a good one.
[crosstalk 00:31:33] Add if not there. Okay,
that's even better. Add if not there. Anyone
else? Everybody in agreement? [crosstalk 00:31:40]
How about append unique? We've got a container,
it's pretty much the same as what you did.
I'm just trying to use the STL style because,
hey, it's there. We want to append the unique
value. There's a container, a value. We do
a fine to c, whether it's in there, if they
find returns end, it wasn't there, and we
push it back, and we're good movers. Then,
we assert it's not empty because that's just
a good thing to do. How hard was that?
We'll come back to this code very quickly.
Why not just deduce the type? There are arguments
why you shouldn't deduce the type. This isn't
just about using auto, this is about, "Oh,
I don't like type deduction. It scares me."
Well, the arguments often fall into the category
of, at least, the ones you hear the most often.
"Well, I can't see the type in my code, therefore,
I worry."
Occasionally that can be a valid thing. I'm
not saying it's not. Most of the time that's
overthinking. In particular, saying, "Oh,
what is the type of something?" Well, first
of all, if you have an IDE, this is the minor
argument because it doesn't really matter,
but if you have an IDE, even Emacs, then it
doesn't matter because you can see the type.
Also, it reflects a bias, which is much more
important to code against implementations,
concrete types rather than capabilities, interfaces.
If you look at that example we just saw that
was hardly unreadable and yet there was no
concrete type mentioned there. Unless you
count void as a concrete type for, yeah, yeah,
to be pedantic.
I call it not a concrete type. It's not a
real type. It's void. It's nothing. There's
no concrete type mentioned here at all. What
type is container or value? "Oh, Well, that's
… I don't know, actually. Just some type
that I can call push back on and call begin
and end on." What's the value? Something I
can move into that containers push back. "Oh,
the container should also have empty, but
when I call dot empty what type does dot empty
return?"
Well, you might say bool, well, who says?
For a long time it returns some weird conversion
to something we will like, but who cares?
You don't want to know. You don't need to
know what it is. There's something testable
like a bool, good enough.
For templates, for return values, we often
don't use those types anyway especially what
we call functions and expressions. We don't
explicitly type those types in our code anyway.
I point this out as an example just maybe
to be less afraid because once we realize
we're already doing this a lot, maybe that
makes it less fearful.
Now, let's talk about correctness. With deduction,
you always get the exact, the right type.
I want a new local variable of his basic type.
If I am now going to take v and call begin
on it, is this a good line of code? Would
that pass check-in? More to the point, would
that pass compilation? Why wouldn't it pass
compilation? [crosstalk 00:34:44]
Const. Right. Well, it should be … Well,
it might be const iterator that would be good.
I could do that instead, but I'd have to think
about it. Now, and if I change the parameter
type for any reason like maybe it's const
today, but then later I change the parameter
to be non-const because my function also does
some modification that it didn't do before
now I've got to go through and do a ripple
to update parameter types if it switches between
say const to non-const. If you just say auto,
which is a great default, it just finds the
right type. It's correct including under maintenance.
Let's talk about maintainability. Using deduction
because it tracks the type makes your code
often more robust under maintenance. It means
that we can actually change our code more
introducing fewer drive-by bugs or other things
that need to be updated while we're updating
our code.
Let's say for example that I changed the code
from line one to line two. I've added a dot
zero, which makes the 42, instead of being
an integer, be a floating-point value. Now,
this code compiles but under maintenance I've
changed that to be floating point and I forgot
to update that variable type, maybe I didn't
intend to have a silent narrowing conversion
there because that's what will happen. The
code compiles and narrows.
If I have a factory that returns a widget
by value and if under maintenance the factory
changes to return a gadget, which happens
to be convertible to a widget then we now
are getting that conversion and we're not
tracking the type. Now, if we really wanted
a widget that's fine. If we want to stay with
widget even if the factory changes to gadget
that's fine, very often we don't.
If I have a map that's iterator and I call
begin on the dictionary, but then I realize
you know what map? Map is tree-ish and I've
heard of this nice hashish. It's got to be
bad Washington joke there. Unordered containers,
I'll just change my map to an unordered map
because most users just work. It's largely
a drop-in replacement for common cases and
this code compiles, and then, fails to compile.
What do I do?
Then, I have the ripple. I have to add unordered
map there because I have the maintenance ripple.
In each case, if I had put auto, I would have
avoided this silent narrowing conversion tracked
the type. If I had said auto in the second
case, I would have continued to use a gadget,
which is often usable as a widget, but I would
have retained the type in its extra characteristics
and with the third auto I would have tracked
the container type when I've used a replacement
container that's otherwise compatible. These
are good features.
Now, a third reason to use type deduction
is performance. Now, this is one of those
ones where if you want to convince a C++ audience,
as long as you can make a credible argument
that, "Hey, do X. it's good for performance.
They'll like jump in a river or a lake, there
could be sharks, it doesn't matter because
they'll swim faster, they're efficient."
Give me performance it's like a will of the
wisp. It's a way to draw in C++ developers.
I'm going to assert that using auto gives
you better performance by default also and
the reason is because you're guaranteed no
conversions. If you use auto without otherwise
as we'll see in the second part talking about
a specific type, you are guaranteed there
are no conversions means no temporary objects,
means no extra work being done that's invisible
that otherwise we sometimes hand ring over.
There's usability, so finally, yes, there
are certain types that are hard to spell.
That are inconvenient to spell, lambdas, binders,
helpers, and unless you want to get really
familiar with that whole type, and I am not
suggesting that, auto is your friend.
Finally, I know I have to say, "It's less
typing." But, you know what? I have to say
that because people notice that, but like
please don't point to this slide out of the
whole talk and say, "That's why Herb is saying
use auto. It's less typing." Like that's the
last least important and least interesting
reason, but, yeah, it's actually less typing
too, which is kind of cool.
Prefer auto x equals expression by default
unless you have a reason to do otherwise,
on variable declarations. It gives you correctness,
clarity, maintainability, performance, simplicity,
and those advantages are something that are
worth something. It also shows you're habitually
programming against implement interfaces,
not implementation, not against concrete types,
but against concepts, which you'll be doing
more of in C++ of the future.
Now, having said that this is all if you don't
need to commit to a certain type. Sometimes
you absolutely do need to commit to a certain
type, then what? You can still use auto, as
I already teased. If you look at the left-hand
code, which is the old style C++98 code sometimes
slightly upgraded for the brace initialization,
but it's the old style of put the type first,
and then, there's the newer style of start
with auto.
The first two lines are essentially the same
so auto s equals "hello." Auto w equals get
widgets. We deduced the type. If you need
to commit, that's if you don't need to commit
to a type. If you do want to commit to a type
try just sticking the type, name on the right.
It will feel weird the first couple of times,
and I think most of you will probably find
that after half an hour you won't even notice
it anymore, but let's talk about reasons and
advantages of doing it this way because those
are pretty much equivalent.
I'll have one slide in the extremely rare
cases where they're not equivalent. Like of
types that you can't even move, but let's
talk about the advantage of doing it this
way. Notice that we already do this for heap
allocation, both old-style heap allocation,
new widget, which, of course, you'd never
want to write anymore, and also, make unique,
make shared of widget just naturally it already
appears on the right because that's where
the type goes whenever we allocate on the
heap.
That's a consistency argument. Does this remind
you of anything else? It's like new style
function declarations, which also put the
return type last or just omit it entirely.
Thanks to C++14. We're seeing a consistency
here and it's not just that the letter A-U-T-O
lineup. There's a deeper consistency here.
Everybody at this point is probably thinking,
"Herb, you are not seriously telling me not
to write int i = 42; are you? Why on earth
would I write auto x = 42? That's a whole
another letter." Also, you could make the
argument just looking at those two only side-by-side
that the first one, the green one, is green,
it's good, its tercer, it's clear, right?
Let's talk about this elephant in the room.
Here are the similar examples that we started
with before and now let's add interest. Int
x = 42, auto x = 42. Without even going further
I could make a consistency argument here.
Some percentage of you wouldn't believe me,
wouldn't agree. That's fine experts can disagree,
but I could make a consistency argument even
going no further, but wait there's more. How
many of you enjoy using literal suffixes for
the built-in types like floats and shorts?
Okay. A few. How many of you enjoyed C++11
user-defined literals? How many of you have
started to maybe even use some of the C++14
standard library user-defined literals? Okay.
There will be more of you. The more of you
there are … Kelly, you implemented them.
Yeah, and the more of you there are the more
this advice will work.
Today and even with C++ 98, well, on the left-hand
side I could write float x = 42., and on the
right, I can say auto x = … f, which is
a floating-point literal. The right side is
better because there's no narrowing. There's
actually a conversion on the left-hand side
that mostly you don't even think about and
never see. I'm not saying it's a big deal.
I'm not saying it's the end of the world.
I'm saying this is the subtle stuff that you
can stop having in your forebrain that you
still know exists, you're not being dumbed
down, but you're avoiding overthinking as
you're expressing the logic you actually want
to express in your code instead of thinking
about the details of a language.
Unsigned long has ul, and I could show some
of the others. Same principle and notice,
again, we're having the type on the right.
This exists already for the built-in suffixes,
but we have also user-defined literals. In
C++14 if you say "42" s that's a std string.
A really convenient way to spell a std string
literal. I just used auto and it's a std string.
Again, those two lines are equivalent the
auto one I argue is clearer. Chrono nanoseconds,
I don't like writing chrono nanoseconds any
more than you do. I don't mind ns at all.
Again, remember functions lambdas, aliases,
I throw lambdas in there too because I mentioned
functions, but of course, lambdas when you
have a named lambda, how do you capture it?
The way you capture it is auto function name
equals my lambda. The signature again is on
the right. That is just the way that you do
it for named lambdas. Notice going beyond
auto a little bit, using, which is the replacement
you should be using now for typedef does the
same thing, and that works even if it's templated.
It does it the same way.
Are you seeing a pattern? When we think about
this, we are in the, well, through a transition
and it still continues, but we're already
well enough through it to see what's happening
where the C++ world is moving to left to right
consistently, at least, by default. You can
still write some of the others if you want
to, but as a default, there's a lot of consistency
across auto variables, literals, user-defined
literals, function declarations, named lambdas.
That's including the generic lambdas, aliases,
template aliases. It's all good.
Now, let's talk performance because some of
you may have been wondering … Well, let's
go back to that first one. If I don't want
to deduce the type I want to track the type
auto x equals value. Well, since I was a young
sprout C++ programmer back in the day and
going uphill both ways through drifts of foot
high snow, six-foot-high snow. That sounds
better.
I know to be suspicious of the equal signs
because they mean assignment copy. Well, first
of all, that's not an assignment, right? Because
that's something we've had to learn just a
quirk of C++ syntax for a long time. That's
an initialization. There's no assignment here.
We're constructing x of some type.
Now, does this create a temporary copy, a
temporary, and then, copy it because in some
syntax it would copy? Does it here? Turns
out standard says, "Nope." That's a good thing.
The reason is if you're some [inaudible 00:46:01],
but it basically has the same meaning. Notice
that tx = a has the same meaning as tx [inaudible
00:46:07] when a has type t or derived from
t.
Turns out when you say auto, you always have
type t or derived from t. In fact, you always
have type t. It deduces it. That is true by
definition. It's a tautology. That equals
costs to exactly zero according to the standard.
Now, if you commit to a type, auto x equals
type value so that's the second one where
you do commit to a type but you can still
use auto. Does that create a temporary and
move from it? Yes.
That copy can be elided and in practice, many
compilers already do elide that temporary,
but full disclosure, yes, the basic language
semantics are that is a temporary and a move,
compilers are getting really good at it. There
is one case I know of where you can't use
auto style, it's where you're using the explicitly
typed version that we just talked about, auto
x equals type name initialization expression.
You have a type that's not movable or cheap
to move. A mute x, an atomic, an array events,
such std array. Those are just a handful of
types where you can't use this idiom, declare
them on the left as always. In the array case,
it will compile, but be slow because it doesn't,
it's not cheap to move, but most of the time
you can use this idiom very well. That's the
only corner case I know of.
For completeness, a few people have suggested
other corner cases. I document them briefly
to show you I'm aware of them with some sketches
of why I don't view these as a counter examples
at all, but let's document the list, and what
you should use instead often in the comment.
One recent time I was resisting using auto,
I was still on the fence selection. This is
auto everywhere, maybe this is too much of
a change. Every new feature gets overused.
I don't want to be that guy. I know I'm going
to be that guy sometimes we all are, but at
least here I consciously don't want to be
that guy, at least, for this particular example.
I wrote base star pb = new derived in my original
group of the week and when I upgraded it,
I left it be unique pointer base pb = make
unique derived. I myself when I would go back
to this article a week later, readers, when
they read the article, would keep on asking,
"Why didn't you just write auto on the left?"
I actually changed it to auto. "Oh, yeah,
that's right. I should have put auto there."
I put auto, and then, I realized, "No. That
changes the meaning because there's a conversion."
It's a make unique derived going to unique
pointer base. If I had followed my own advice,
which now I feel much better and I do, and
simply stick to my guns and put auto first.
Yes. That's an unusual construction the first
time you see it, and then, you realize, "Oh,
it's just auto variable equals type and if
there could be a type, caste expression or
a constructor explicit type on the right-hand
side.
Then, it's perfectly clear. Just moving it
that much over made it perfectly clear, at
least, to me, and a number of actual readers
that the code isn't a bug. "Oh, yeah, you're
actually doing a conversion there." We just
find in practice the middle one was too easy
to miss, even though now we're all aware of
it come back a week from now and take a quick
glance and you might roll the dice on whether
you remember. It's just that much distance
makes it subtle.
Preferred declaring local variables using
auto, whether you want to track. It's just
auto and bind directly to the type or stick
to a specific type, in which case you want
to explicitly mention a type but you can still
use auto. Here are the two syntaxes. It guarantees
no implicit conversions, no narrowing conversions,
and there's one thing I mentioned on the slide,
but I forgot to say in words so let me add
it now. If you follow this style it is impossible
to write an uninitialized variable. Why?
Because when you say auto x equals you, auto
is meaningless without an initializer. It
is impossible syntactically to write the bug
of uninitialized variable, which is not bad.
Consider having some functions in headers
also use auto. The new C++14 feature you can
have auto be the return type and deduce it
from the body just like lambdas can. That's
actually a nice thing to do there in headers
anyway. You can do that and the bodies have
to be available, and you want them to track
the type so auto is often a very good return
type for those. Since we're talking about
auto, I thought I should mention return types
as well.
It's simple. We can tell Nigel Tufnell and
all the other C++ developers in the world,
who agrees, that to make a type track deduce,
to make a type stick commit to it, in both
cases, you can still use auto, try it. You
might find that it's consistent, simple, and
not complex. Give it a shot.
Now, let's turn to a specific area of overthinking.
When you think of how much the C++ has evolved
since C++ 98, if you look at the 11 and 14
standards, you could point to a few features
as being big ones that really change the game
in terms of how the type works and how our
idioms might work. Probably the biggest of
those is rvalue references and move semantics.
The idea that we have move semantics, C++
always has be able to deal very well with
copyable types, value types, and really move
is largely an optimization of copy.
Yes, you can also write move only types now,
which is a new thing, but largely it's an
optimization of copy, which means copyable
types that C++ has always been so good at
compared to many other languages that are
mainstream just got even better, but what
this means is the community needed some time
to absorb. Okay. Well, what does that mean
for my programming style? We needed experience
from trying to develop guidelines, seeing
which one's worked out which ones didn't.
I think that this summer, and in particular,
in the last couple of weeks, and in particular,
this week at this conference that we're ready
to say, "Okay. We have got some pretty good
guidance specifically on how does move semantics
and the other features that are new in C++11
and 14 effect parameter passing. I'm going
to go out on a limb and debunk what I think
are some antipatterns and why I think that
they're antipatterns, and why the C++ 98 default
actually should get a lot of love in C++14.
One subtle change that was already happening
even before C++11 because compilers now routinely
implement the return value optimization, use
return value, return by value way more often.
You'll see me mention that, but don't overuse
pass by value. I'll explain what I mean.
Observation, Bjarne is very right. New features
tend to get overused and Scott made a very
interesting point as we've been having discussions
over the last couple of weeks about what are
the right parameter passing guidance guidelines
because a lot of people have said since we've
got move semantics maybe we should pass by
value more, maybe we should do other things
to try to get those juicy rvalues that now
we have access to, right?
Like now, we have access to rvalues let's
optimize for those. Now, we're mesmerized
and we're going off on that. We have had such
a focus on rvalues that when Howard Hinnant
and others have come up saying, "Oh, by the
way here the, if you do that way you're causing
these problems," which I'll show you in a
moment. One of the realizations Scott put
very well is, "Oh, you know what? Those problems
are with lvalues. We've been focused so much
on optimizing rvalues, we're not noticing
how often those things are actually pessimizing
normal passing variables to functions.
I'll show you what I mean. What's important
here is two things. The advice is still simple.
In fact, it's exactly the same as C++ 98,
the default advice that I'm going to show
you, and second, you have to think about all
of the things your callers might give you,
not just focus on the new thing such rvalues.
Just as exception safety is not about writing
try and catch everywhere, using move semantics
is not about writing move and rvalue references
everywhere.
In fact, remember I said that in Bjarne's
keynote on Tuesday, rvalue reference didn't
show up once in his slides, it will also not
show up once in our default parameter passing
advice. Let's see. First of all, thanks to
many, many people including more not listed
here for discussions over the summer and leading
up to this to firm up. Here's a great opportunity
with cppcon. Let's talk about this and talk
it through and see if we can converge on new
parameter passing advice.
I believe that we have time will tell the
level of agreement, but I believe we have,
and so, I'm very glad to acknowledge these
people and more for the input on what's to
follow. Here is parameter passing advice in
C++98 that is reasonable default advice to
give people. On the vertical axis we have,
what are you doing with the parameter? Is
an out pointer, is it an out, in out, in,
and in particular is it in, and then, I'll
keep a copy. It turns out to be an interesting
case to call out especially.
Across, we have, well, is the type cheap to
copy because I maybe I'll pass it by value
more often. Is it moderate cost a copy or
maybe I don't know or is it expensive to copy
like some big data structure? For the out
case, we've more and more because we have
the return value optimization in modern compilers,
we can just pass by return by value for moderate
cost, and you'll notice the footnote, moderate
cost means, yeah, if it's like a k and it's
contiguous and hot and cash, yeah, just passed
my value or just returned by value.
That's a new thing in the last 10 years, but
we're about 5, 10 years in now to where you
can assume your compiler does that optimization
so you can actually return by value in all
those cases. If it is expensive to copy like
a vector or a bigger array then the C++98
advice was passed it by reference. That was
really like the in-out case or footnote returned
an x star, allocated on the heap and return
it, but that costs you a memory allocation,
you have to decide if it's worth it so I mentioned
it as a footnote, but generally, it's the
reference is what you use.
In-out is easy, of course, passed by non-const
reference because that way you can modify
the original, and finally, for all the in
cases, usually passed by const reference,
but, hey, if it's an int, pass it by value.
None of this should be news except possibly
that for the out case we can return larger
objects than maybe 10 years ago we would have
wanted to do, but this is journeyman. This
is not surprising advice anymore C++98 advice.
What's the difference in C++14 then? Modern
C++ advice.
Are you ready for the difference? Don't blink,
you might miss it. There it is. Let me go
back and forth. See the difference? The main
difference is the advice is still the same,
but we're move has come in is not changing
how you pass your parameters, but the applicability
of the advice, the default advice actually
becomes even more usable because more types
fall to the left and out of the out parameter
exception. In particular, if the first column
is now, is it cheap or impossible to copy
such as unique pointer. It might be impossible
to copy. That's a new thing.
The middle column is now is it cheap to move
or moderate cost to move. Notice vector has
now moved into the middle where it used to
be on the right-hand side. The defaults have
become more usable, and if it's still expensive
to move, not copy now, but expensive to move,
which is a smaller set of types they're still
on the right-hand side, but that exception
case has been shrinking, which is kind of
nice.
Again, back, forward. This is the slide that
I would like you to print out and pin to your
cubicle wall, not the next slide. The next
slide is going to be, and here's the, if you
open the hood, here's advanced advice that
will be the one you're tempted to save. Well,
I want the advanced stuff so I'm going to
pin that one to my cubicle wall. Please, pin
this one to the wall. The summary, and here
this is a direct quote from Bjarne as we were
working through this. "You know the defaults
just worked better." This is still the right
advice.
That's Bjarne's opinion, it's my opinion,
and I think that even Scott and I agree entirely
on this with the exception that he wants that
one extra line on the next slide to be on
the first slide too. The in and move from
and I don't, I still don't think it's worth
putting in the default advice because the
default advice, we just talked about covers
all types including move only types, and includes
passing unique pointer by value just like
I said earlier to a sync function.
Now, if you want to optimize C++ is even better
for giving you advanced knobs, and now, this
is where you see rvalue references come in
because notice the in entertain a copy line
in the middle part. Now, on the previous slide
that said passed by const reference, right?
Reference to const. Now, it's, and by the
way, if you want to optimize for rvalues,
add an overload of x refref, rvalue reference.
For the new case if we want to break out the
explicit case of, "Okay. I'm going to pass
in a value and move from it then as an advance
knob if you want to optimize that, you can
pass x refref for unique pointer for example
that often won't be an optimization.
Actually writing a unique pointer refref as
a parameter often will not actually be an
optimization and the reason is because if
you pass an actual temporary in by value to
the unique pointer. That's a move, and then,
you move out from it again, and then, inside
the function, so you say, "Oh, that's move
plus move." If I do unique pointer refref
that's a single move, right? Faster, must
do it, hold on, wait.
Compilers can optimize that. They do. In practice,
even by value, it's just a single move, and
besides, if you're worried about a unique
pointer move that's like moving a raw pointer
like copying a raw pointer. I do not believe
for a moment that 99.99, maybe 99% of code
will ever find that in the hot loop, but hey
if you want to optimize it move for other
types that are movable, move only, but maybe
more expensive the new pointer, do know about
that one.
Now, a few comments about this. First of all
notice, there's a red as in danger, dragons
be here, special case. Yes, for those middle
cases at the bottom. You can also write perfect
forwarding in certain cases. Well, you could
write that if you were able to write it. You'll
see it mentioned on a later slide, but we'll
warn you before it comes up on the screen.
Bjarne summary, which I agree with is defaults
work better and there are more optimization
opportunities than we had before, but remember
they're optimizations, therefore, don't reach
for them prematurely. That's just good software
design.
A note here, you might be thinking, "Well,
but I have to write refref on my move assignment
and move constructors for my class." Herb,
how can you get up on this stage and tell
me with a straight face here in … that I
shouldn't be writing refref in normal code.
This is a subtle point, but it's actually
a really cool one, I think. Writing refref
is nevertheless an optimization, but the advice
that we give here in the entertaining copy
line is exactly what you write for your constructors.
You overload them on const x ref and x refref.
It's exactly what your right to express the
lvalue in this and rvalue in this for move
assignment versus copy assignment. You overload
on const ref and at const refref, and don't
do that unless it's an optimization. Now,
the thing is if you're writing a type that's
going to be widely used, now your library
writer hat and you will have many users it
makes sense to reach for such optimizations
by default, but this is actually entirely
consistent advice it's just when you're writing
a class, you'll reach for optimizations more
often because you're supporting a larger audience.
That is normal.
Of course, and especially in the standard
library that's why you see this all the time
in a standard library even perfect forwarding
in STL implementations because they're so
widely used. As you go further down, you reach
for optimizations more, but it's the same
advice as for any other code, overload on
refref if you're going to move from it.
Now, there is one drawback to that. Let's
talk about these options. Again, more optimization
opportunities. Those are the things that are
new. When do you write, and I have to say
rvalue refref? Again, like I just said, "Write
it only to optimize rvalues, which could be
move assignment operators, move constructors,
but just as the exception safety isn't about
writing, trying catch a lot, move semantics,
using move semantics isn't about writing,
move, and refref a lot just return by value
move will kick in, pass by value move will
kick in, but wait, let's dig into an example.
Because one option that's been discussed quite
a bit since 2009 has been, well, hey, move
semantics are coming. Now, move semantics
are here. Maybe we should pass by value more
often and there's a really nice article that
does a detailed analysis of a compiler optimization,
how they interact, and if you pass by value
you can accept lvalues and rvalues, named
objects and temporaries, and it will move
from the ladder, great. You'll still get a
copy for the former, this is for the in and
retain a copy case.
Should I use f of x? Pass by value here. How
many of you have seen the advice to do that?
Okay. You will want to pay close attention
to this nice reasoning. It's interesting this
advice first was as a result as near as I
can tell of Howard Hinnant, a very experienced
library developer. I did most of the [inaudible
01:04:41] libc++ implementation, and metro
works before that and much more, who noticed
many of these optimizations, and then, told
other people about them, and so, those other
people popularized this advice, but Howard
himself has been pointing out, but wait there
be dragons there too. There are downsides
to doing this, and so, I especially want to
call out to Howard.
Thank you for educating me this summer about
the information to follow about why this advice
is actually problematic and what you generally
don't want to do, but there's one specific
case where it's useful. The good news is this
can, in fact, be faster than C++98. For the
in and I'm going to keep a copy case because
I'm going to move from rvalues. I still get
to write only one function. This is good,
but it can all so be much slower than C++98.
Now, here's the reason. If I'm being passed
an lvalue, a named object. Remember, this
is what made Scott say, "Oh, yeah. It's about
the lvalues." We're focusing so much on optimizing
rvalues, what's the cost of lvalues? If I'm
being passed a named object here, I am going
to unconditionally copy it, and if inside
the function I'm going to move it then into
an existing string that move into the existing
string, the move is an assignment, and it
means I've unconditionally copied, and then,
assigned.
Now, what if that string if x is a string
or what if that vector if x is a vector is
smaller than the capacity of the thing I'm
eventually going to move it into. Well, if
I had just assigned it directly I would have
reused the capacity. It's much more efficient.
I would have had not had a memory allocation
because the destination already had the capacity,
but because I express f of x upfront, especially
in cases like large strings and a vector that
might have a capacity already. I cannot reuse
it because I'm forcing a deep copy and a memory
allocation unconditionally, and then, moving
in.
Let's take a look at examples and numbers.
Here's an example that it would be a crime,
it would be a horrible thing if we couldn't
give a simple answer to this question, and
yet I have seen people, I have seen them have
long arguments about how do we answer, how
do I write set name? I won't share all of
the answers with you, but some of them are
so long that I'm reminded of Bjarne Stroustrup's
words that speaking of how C++ is being taught
by some people, and in particular for teaching
C++ using C first, but that's not the only
one.
He says, "If that's C++, I don't like it either."
We have to have a simple answer to this. How
on earth could we have a modern language where
we can't say employee has a string name and
I have a set name function? There should be
a simple answer, and there is, to take that
parameter, to change the names with the new
value, but there's been a lot of overthinking
going on. Let's step through it slowly not
because it's hard, but to dispel some overthinking
that has been happening.
Option one is the answer. You already saw
the slide. It said, in this case, the n plus
copy case unless doesn't enter something,
pass it by const ref. You already knew the
answer from the previous slide, at least,
you knew the answer I was going to give. Just
pass const string ref, and you know what?
That's the same answer as C++98. There ain't
nothing new here.
Let's analyze it a bit. There's always going
to be a copy assignment here, but usually
much less than 50% of them will perform an
allocation. If it's a small string, which
are many systems is 16 or 24 length or 11
or something like that, the std string implementation
every major std string implementation already
implements the optimization that small strings
are stored in the string object itself using
a union trick basically, instead of doing
a heap allocation, which means that you can
use lots of strings in your program and never
have a single heap allocation, and if you've
wondered why, small string optimization.
It turns out this is a good thing to optimize
not overly prematurely in the library because
there are lots of studies showing that the
vast majority of strings in many modern, many
mainstream applications are short. It makes
perfect sense to optimize for those. Even
if it's a large string this still performs
a memo allocation less than 50% of the time,
right? It's only going to perform an allocation
if the new string is longer than the capacity
of the current string, which means that if
you call this in a loop it's going to allocate
a few times, and then, stop allocating.
Let me say that again. If you call this in
a loop, you call this repeatedly, it's going
to allocate a few times, and then, stop allocating
ever unless you happen to hit a really long
string one, and then, you'll get that capacity,
get one more outlier. What if I want to optimize
this for rvalues? Because here I'm not optimizing
for rvalues, so that's okay. We had advice
on the advanced guidance slide just overload
on string refref, on the rvalue reference
to a string, then, move inside the body of
the function.
By the way, notice I also added noexcept because
it's important to think about exceptional
safety. The first function that takes a const
string ref it can't be noexcept, why not?
[crosstalk 01:10:04] It's good copy. It might
actually do memory allocation. It will do
fewer of them and not many over time, but
it might do some it could throw, but the second
one ain't going to throw. Just taking ownership
of the buffer already owned and I'm going
to delete any buffer I might have already
had, but that's okay because that's no throw.
This is actually nice I couldn't optimize
for rvalues so write this by default and if
you aren’t optimized for rvalues, if you
have performance data that you should then
add the overload for name. Now, a couple of
notes. When you pass a name to object it's
one copy assignment as before, but now if
you passed a temporary it's one move assignment,
which is roughly … I actually tested and
benchmarked the three common STL implementations
and the move assignment is about the same
as copying for five ints. It's pretty much
dirt cheap. There's no allocation, therefore,
we can make it noexcept.
Now, I will throw, there is one downside to
this that actually in practice almost never
happens, but there's one case where it does.
I'm going to come back to this, but I do want
to be truthful with you and point out the
downside. If you do this, overloading on const
ref and rvalue reference if you have more
than one such a parameter and the N+ copy
parameter, is combinatorial. It's like, "Oh,
you mean if I have like three I need to write
eight function?" Yeah. Yeah. Have a nice day.
We don't like that. Now, the good news is
that almost never happens except in constructors.
Hold on for that. We're constructing an object
that has saved many pieces of initial state,
but in most set like functions, we're usually
sending only one value. This is perfectly
good advice, standard library does it all
the time. This is pushback. You're looking
at pushback basically and others like it.
Now, let's talk about that option that seemed
like the shiny object and that actually did
seem maybe like a good idea, maybe something
that we should explore as a new guideline,
pass by value. Now, let's think about what
happens here. If I pass my value, first of
all, absolutely, this is simpler code because
I get a single function. I completely agree
this is simpler code than option two, okay,
and right for simplicity first. I agree with
that. I don't think it's simpler than option
one, therefore, you should write option one,
but let's analyze option three.
I passed string by value, I move from it inside,
and if I pass a named object I get a copy
construction of the string, which means I'm
going to unconditionally allocate if that
is a long string if it is longer than the
short string optimization, and then, move
a sign. That means I can never reuse the capacity
already inside name. If I passed a temporary
it's good because then I just have one move
assignment, no allocation, it's noexcept.
You notice that noexcept is not green. On
the previous slide that noexcept it was green.
Green is soft and cuddly. Reminds you of freshly
cut lawn in the summertime, butterflies dancing
nearby, but this noexcept it's kind of dark
in the alley where this noexcept lives. This
noexcept is problematic. I'll overstate it,
but I'll overstate it just only slightly.
This noexcept is a lie. It is technically
true, your code will compile, it will never
fire. It is actually technically following
the rules of noexcept, and I view this as
nevertheless a lie.
Think about what it's saying. It is saying
that that function never throws. Now on what
basis can it say that? Well, in the body,
nothing can throw. That's perfectly true.
Why? Because we have pushed the operation
that might throw into the caller. If he has
an lvalue and he calls this function a copy
will be performed. That copy could perform
an allocation. That allocation could fail.
That could throw, but by saying no throw,
we're saying, "Yeah. Yeah." But that's before
you actually got to us. It's your problem.
Pity the caller, if we, I want to points out
that if we tell people to do this a lot, I
am certain you will see people who have, look,
"How so hard C++ is. Here's a puzzler for
you. I write a program that calls only noexcept
function and throws. Hahaha." Don't be that
guy or at least understand what that noexcept
means. If you're going to do this, and there's
one case where I'm going to suggest you do
this, I strongly suggest not writing the noexcept
even though you legally could, and it would
never actually fire because it's alive for
the reason we just gave. This is at least
problematically. I'll call it mendacity.
Finally … Oh, hold on. We're all adults
in the room, but this is being recorded and
we're going to have an online audience so
those of you who have parents, children in
the room you might want to get them out of
the room. We're going to have some graphic
imagery here now. Give you a moment. Okay,
those of you in the room you may want to cover
your eyes. Are you ready? This could be disturbing
to some viewers.
Actually, oh, that's cool. I actually heard
literal gasp. It if you want to write a perfect
forwarder, you can write mostly just the greenish
parts, but you still need to template class
stirring it, but you could probably ignore
the enable if is same decay T and the noexcept
but you shouldn't. If you're actually going
to write a perfect forwarder, write it reasonably
perfectly. Like, write it right. If you were
going to write a perfect forwarder then I
submit you should be among that small dot
of developers in the world that you could
barely see on the first slide earlier on because
it was only a couple of pixels, compared to
the millions of C++ developers, but let's
talk about it.
It's optimized to steal from rvalues and much
more. It's entire, it's for perfectly forwarding
whatever you pass it through, you have to
make it a template so it can take anything,
but then constrain it to strings. We're actually
going to name the template parameter string
then constrain it, and if you pass a named
object, there's one copy assignment as before,
which is optimal. Pass a temporary one move
assignment, noexcept, it's optimal so we actually
expressed the noexcept with the noexcept std
is no throw assignable, std string ref to
string called cone value.
The quote unteachable comes from Bjarne Stroustrup.
I agree this is not teachable. This is an
option for advanced developers that if you
are a very advanced developer, sure, know
about this, and if you might see it in your
standard library implementation I hope will
not see this in much production code because
it's for very low level of uses. Notice that
some drawbacks, it generates lots of functions
because it stamps out the template for everything
you might call. It must be in a header, can't
be virtual.
I don’t usually hear people mention this
stuff when they like the … "Look, perfect
forwarding. Ooh." I don't normally hear them
say, "Oh, but it can't be virtual, has to
be in the header." Generates lots of function.
I just don't hear that. I'm actually less
worried about the generates lots of functions.
I'm just pointing it out because it does stamp
out, it's a template, but most people over
obsess about template stamping out functions.
I never actually hear bug reports about that,
about code bloat. That seems to be more of
an urban myth than a reality that that's an
issue, but the fact that the code has to be
exposed in the header is an issue. That it
can't be virtual is an issue.
Now, I already answered this question, but
let me, but let me lead up to it with a couple
of questions. How many here believe that they
could write this code confidently? Okay. There's
still some hands up. Quick somebody, who's
got his hand up shoutout, does this accept
a string literal or not?
Male: Does not.
Herb Sutter: Does not.
Male: [inaudible 01:18:11]
Herb Sutter: I've constrained it wrong. This
is Howard's code. [crosstalk 01:18:17] I would
love to hear why it's constrained wrong because
this is Howard's code and if he can't write
it, we can't either. By the way, I will mention
one thing about this code, I actually asked
Bjarne in person. What about the way you think
about this? In the same breath that he said,
"This is unteachable." He said that in terms
of writing this with confidence, he says,
"There are very few people in the world who
correct this with confidence and he didn't
think he was one of them."
He could look it up. He's certainly capable
of understanding it, but just write it with
confidence without thinking, without looking
something up. Now, let's talk about performance
because the reason we're reaching for this
toughest performance, right? Let's measure.
Here are options one, two, three, and four
passing a const string ref, that's option
one.
The default that I'm saying you should always
do is the first bar. The optimization for
rvalues is the second bar. Then, option three,
which is that new thing about pass by value
and just have the one function that's option
three, and then, perfect forwarding is option
four. Here is benchmark code for that employee
set name function called in a loop so it absolutely
is exercising the case where it's reassigned
in the variable a number of times so every
hundred times through the loop it makes a
new employee object, but we are reassigning
99 out of 100 times.
Do you notice any performance glitch in this?
Notice options one and two are fine for lvalues
and four for the char stars, but if you go
and use option three, as long as you're in
the first case, which is small lvalue so you're
having, you're passing a short named string,
1 to 10 characters to make sure you fit in
the single, the small string optimization.
You're good.
In the second case, we're passing larger strings
varying from size 1 to 50 because most of
those will have to be allocated on the heap
somehow. That is the case where you see that
spike and only for option three. This is the
problem we were talking about where if you
just pass by value, you must allocate on every
path for a long string and the same is true
of a vector, anything else that could reuse
capacity, whereas in the other cases we could
reuse the capacity.
Of course, allocating from a char star and
having to do now an allocation costs you more,
but notice everything's pretty cheap except
here is a big pessimization and actual data
for if you use option three, here's a case
where it's a pessimization. It's not always
going to be, but if you're not sure measure.
I did. You might notice that the top of the
slide says visual C++. This is probably a
Microsoft problem. It's probably STL's fault,
right?
Actually, it's not. We're going to optimize
string some more, but this spike is not STL's
fault because Howard also graciously ran this
on his own box in clang and lives to the SQL+.
Yes, I changed the slide. Let me go back so
you can see I change the slide. Same problem
with Clang and libc++. STL being no worse
than Howard is a really, really high compliment
plus this is unavoidable because you must
allocate memory for the long strings for that
case.
Now, how many of you use GCC? I have good
news for you, kind of. Here's libc++. Is this
good? Warning trick question. [crosstalk 01:21:57]
Because it still does the copy-on-write optimization,
which is no longer conforming and has other
problems that would show up in different benchmarks
just not this one, but notice there's no spike
like compare, "Oh, look, Clang, GCC, better.
Aw."
Well, yeah, except it's non-conforming, but
that aside and the fact that the other things
are slower. I'm just making a little fun.
The GCC folks are great. They've known about
this problem for a long time. In fact, in
the box, they already ship what's going to
be the new basic string as soon as they can
take an [inaudible 01:22:34] breaking change.
They've been deferring it and deferring it,
but in the box since something like GCC 4.1
or something like that. I forget exactly,
there's something called vstring. How many
of you by the way use vstring?
Yeah, think about it. It's pretty good. I
see one or two hands. Are you ready for vstring
from that same current build of GCC? Oh, look,
there's our friend again. This is a pessimization
that most of the people who were, had a fling
with passing the string by value advice didn't
take into account and in fairness that wasn't
really well understood, and one of the reasons
when I mentioned to Howard that I was giving
this talk and I wanted to give this advice,
he actually thanked me for drawing attention
to it because very few people he said seem
to know about this. It seemed to be worth
sharing some numbers with you here.
For vector and large strings, keep in mind
that the cost of the special member function
is not necessarily what you expect. For many
classes likes a shared pointer, which is more
expensive to copy construction or a move construction?
Copy construction, right? Because you're at
an increment, decrement, the reference count,
which you don't have to do it with move and,
but assignment as well. Many classes have
copy and assignment and move have different
costs. Some classes they cost more, some classes
they cost less.
Remember, when you copy, you also have to
get rid of the previous state, when you move
you don't. Here in this particular case, for
vectors and large strings, the cheapest thing
you can do, in general, is to move a sign
from it. Move assignment is actually cheaper
than move construction especially in the cases
where you get to reuse the buffer. This is
not the case say for shared pointer where
being able to, construction costs more than,
or assignment costs more than construction
because you have to get rid of the old state.
Many classes these are reversed, but for vectors
and large strings it turns out that the assignment
is actually cheaper than the construction,
and so, by taking my value we incur that copy
construction followed by move assignment.
Well, that's the most expensive operation
in many cases for vectors and large strings.
These numbers don't generalize to all types
measure, but it's worth knowing about. Thank
you, Howard, for pointing out the construction
and assignment do not always have the same
costs. Here's a nice quote from him that you
can read, and this comes back also to Occam's
razor, don't multiply entities. He was talking
about logical entities, but it applies also
to objects, needlessly, and Scott and Andre
have long said, "Hey, don't ask for work you
don't need."
In particular, Scott is recently, rightly,
teaching a lot that it's a bad habit. It just
create lots of extra objects. We want to avoid
that. Remember this is about default. It's
not about not thinking, but it's not overthinking.
It's actually not hard right for clarity and
correctness first.
Now, there is one place I said where you might
want to pass by value, one case. That's constructors,
and it's for two reasons. Look at class employee,
it might have a name and an address in the
city and when I construct I have to pass all
of those things. That's the case that's bad
for the overload const ref and refref because
it's combinatorial, but it still avoids the
case that's bad for option three because I
am constructing those strings, which means
there's no capacity that I'm missing reusing.
I'm constructing them, not assigning it to
them.
I don't care about not reusing capacity because
this avoids the pessimization in option three
in other cases, and it is the case, the one
case where you typically do have multiple
value parameter setting for constructors do
consider option three, but, again, as an optimization.
Normally, const ref is fine, but if you do
want to optimize for rvalues in that one case,
consider this optimization. This is fairly
new knowledge.
By default, pass const string ref. To optimize
at an overload that's noexcept often for rvalue
reference. That's general default advice.
One more thing, when you have, when you see
refref and the type in front of it is a template,
what do we call that? Read the slide. What
do we call that today? I'm hearing people
say universal reference and that is a term
that Scott Myers has rightly popularized.
He has rightly popularized that we need a
name for this and nobody else has stepped
up to give it a name.
I am going to suggest that we call it a forwarding
reference. One of the nice advantages of being
here at cppcon is that we've had a chance
to have discussions about this. Let me ramp
you up on some discussions that have happened
here this week. Again, just to motivate this
in case you're wondering about the difference,
let's say I have class types foo and bar.
One is templated and one isn't. Those are
very, very different refrefs.
In the first case, it takes an rvalue reference
to non-const. It only takes rvalues and the
reason it's there is to capture temporaries.
In the second case, it takes a mumble reference.
Okay. They say what we've been calling a universal
reference, but I'm now recommending a forwarding
reference. It takes it to everything. Notice
I didn't say to anything, I said to everything
because it will stamp out the right thing.
It will take const. It will be volatile. It
will be both. It will be neither, it accepts
all y objects, lvalues, rvalues, anything
you give it, and the reason that it exists
is not to optimize anything. It's a very different
reason, it's too forward.
The reason that it exists is to take whatever
you give it, that's why it's so flexible,
and just pass it on. I don't care if it's
const of all because I'm not going to use
it myself. I'm going to pass it on to somebody
who is going to use it and he will know or
care, whether it's const and so forth. [crosstalk
01:29:05] Forwarding references is … Well,
what I'm going to suggest should be the new
name for this. Several of us have talked about
it this week, Scott has rightly pointed out
that this t refref is different from other
other refref parameters.
It is not an rvalue reference. We do need
a name for it. He coined that name and he
uses it extensively in his book whose final
galleys are due like in like three hours,
which is a little inconvenient because you're
not supposed to make big changes and you just
can't make big changes in the book as it's
going to press [inaudible 01:29:41] just can't
slip. We talked about it at cppcon because
we were all here, it was a great opportunity,
and this is one of the big benefits just being
able to talk with each other. Scott and I
talked, Bjarne, Gaby, others, STL, and we
haven't talked to everybody yet so this still
has to be vetted by the community and the
committee as to whether people agree that
this is a good term.
But we all agree that the word forwarding
reference is better and avoids, and that universal
reference, the concern is that it makes people
think it's universal, I should use it all
the time. It doesn't describe what it's for.
Forwarding we believe is exactly the right
name because it says exactly what it's for,
what you do with it as well as giving it a
unique name.
We think the right name is forwarding reference,
in the meantime, Scott is going to change
his book as it goes to galleys. Thank you
very much, Scott, for doing this. That is
a big imposition and he's being very gracious
so that he's not changing it universally,
but he's adding a footnote and an index referencing
by the way it looks like forwarding reference
is a better term that's going to get traction
in the community if and when that actually
does happen, which I believe it will. I know
several people who are going to keep using
that from now on.
Then, you will find new printings of effective
modern C++ will switch to that term instead.
FYI, this is, we want to thank Scott very
much for popularizing that, hey, putting a
shining light on this needs to be taught differently,
this works differently, and that has prompted
us to say, "Okay, let's try to come up with
a good name. The one that we can live with
for the next 20 years." Use refref only for
parameter return types, rvalue references
to optimize rvalues like we said or t refref,
there's that term forwarding references.
One dessert slide. Did you know that C++ now
has multiple return values? Just return a
tuple or a tuple if you're Scandinavian. I
don't know what that means. That just came
out. Sweet realization. We're already doing
it. If you in C++98 had a set of string and
you insert into it, what does it give you
back? It gives you back a pair of an iterator
and bool. You would say if result dot second
so if it succeeded then do something with
result dot first, right? That's just the convention
that we used. Now, here's a really cool thing.
That code exists in the wild, it's been there
for 20 years, right?
Ready? Watch this. C++11 gives you auto. It
immediately works nicely with this. You don't
have to type that big long thing, but wait
it gets better. Just hold on one second, but
we have this nice backward compatibility with
it because we could just accept that return
value and say dot first and dot second as
before, but in C++11 we also have a new feature,
which works well with that old feature. We
have tie for tuple and pair. Anything that
a tuple can do has generally been added to
pair as well as the case of a tuple of length
two.
They're pretty much consistent. The library
group did a great job of that, which means
that I can call that old insert function unset,
unchanged from C++98 and tie two variables,
an iterator, and a bool and it works great.
I don't have to say first and second anymore,
I tie. Iterator and success equals function
call, multiple return values. If success do
something with either.
The only thing I can't do is declare a variable
inside tie and people are writing a proposal
for that because that the next thing they
want. [crosstalk 01:33:07] That's pretty cool.
It works with the existing STL and with your
existing code if you have multiple return
values express them with a tuple. Remember
it's hard to remember you're an expert. Can
I just have a show of hands? Like how many
of you are students here? We have a number
of students here.
Actually, can you please stand up? Stand up
if you're a student, please, and please stay
standing. Yeah. We're glad to have you here.
Now, please, please stay standing and anybody
who has been using C++ for five years or less,
please join them. Please, also stand up. Five
years or less using C++. Please, stand up
and stay standing. Thank you.
Now, if we can get Chandler up here, while
you're standing, is it okay if I take a selfie
with you, guys? Okay. Chandler where are you?
Okay. I'm going to come down there. please,
stay standing and I want to get a picture
of you guys because this shows we need to
keep everyone in mind when we're teaching
C++ and welcome all the people who are joining
our community. Is this a good place to stand?
Male: Where did the [inaudible 01:34:26] go?
[crosstalk 01:34:29]
Herb Sutter: I'll go where he tells me. This
isn't the first time. Thank you very much.
Thank you, Chandler. Thank you for accommodating,
and welcome all of you new folks to C++. I've
decided I want photographic evidence to keep
showing people on the committee and other
places all the people who are new. We welcome
you to C++ as a fresh new language, simpler
than ever. Sometimes we are addicted to too
much complexity, but there are very simple
reasonable defaults simpler than ever for
loops, pointers, and references.
Smart pointers, never write delete again.
Variable declarations, parameter passing,
if you have a couple of questions please line
up at the mics. We'll take one or two, and
then, we'll break for lunch. Then, come back
here for two o'clock and we'll have our final
panel as well. Here, please.
Male: Yeah. In the spirit of simplicity, which
I absolutely agree with, are we looking to
simplify the algorithms in STL so I don't
have to put begin and end everywhere?
Herb Sutter: Yes. We are looking at simplifying
algorithms, range based algorithms will be
wonderful. There's some real proposals coming.
Male: I was wondering if you could go back
to the benchmark, any of the benchmarks. Well,
could you like …
Herb Sutter: It's all the way back here. Oh,
you know what? I could do this this way. How
about GCC? They're good.
Male: It's fine. Okay. In the last example
in the right, there is something that confuses
me basically option four is much faster than
option two, which shouldn't [inaudible 01:36:23]
like match option two.
Herb Sutter: These were the results as measured
on that system.
Male: I mean it isn't like …
Herb Sutter: You can download that code and
try the test, run it yourself. I showed you
pretty much the code. Check the assembler.
I did not inspect the assembler as to why
on the right-hand side they were different.
This is what I measured. In fact, if you go
to the others you'll find the same thing.
The perfect forwarding is faster even for
characters stars than the others.
Male: Yeah, but it makes no sense to me because
we know [crosstalk 01:36:55] …
Herb Sutter: Goat and Stefan is waving and
saying talk to him and he will enlighten you.
Male: I have one last question and …
Herb Sutter: We're going to switch over to
here. Thank you very much. Next.
Male: I want to say forwarding a char star
two operator equals should actually be fast
I think. My question was about at the end,
you do this in there std tie and earlier one
of your first points was if I say auto x equals
something that I no longer have uninitialized
variables, but today if I want to use std
tie for that purpose, don't I have to have
uninitialized variables?
Herb Sutter: Well, you don't. If you use std
tie … No. No. No. You can't use tie as a
reason to have uninitialized variables. Yes,
you have to have separately initialized variables.
You have to declare them somewhere else. That's
why I said the next things that some people
want to have for tie is to be able to declare
variables in there, right in the tie for the
reason you just said so you still have to
… Yeah, wouldn't that be great, but you
still have … C++ is evolving, man. We're
going faster and, but you still put them in
the local scope.
Before you still have to declare them, but
if you don't want to initialize them that's
not what I'm telling you. You still use auto
x equals whatever and even if it's zero.
Male: Okay.
Herb Sutter: One more, and then, let's go
to lunch.
Male: I love that you started with all this
avoid complexity and all that and that you
started with the quote from I think Bjarne
of we have a tendency to use new things just
because they're new. I am not sold on your
arguments for auto. Your second argument for
auto about allowing your code to be more maintainable,
I would really like to have examples of a
large meaning, not large but meaning full-sized
code … trick of changing types of return
function or return … return types and the
like …
Herb Sutter: Let me ask you a question.
Male: … without having to do any work.
Herb Sutter: Let me ask you question. How
many here have ever in their code bases changed
a std map, the tree based one to a std unordered
map? There's some data for you.
Male: Yeah, but you have to [crosstalk 01:39:01]
…
Herb Sutter: The iterator type changes.
Male: Yes. Clearly, but you're going to have
to test this while you're doing it.
Herb Sutter: Oh, yeah. Sure. The question
is not whether you still have to test it,
the question is, A, is there a code ripple
that you have to do it manually or will the
compiler do it for you? Second, the correctness
of if you get a type where actually the compiler
won't tell you that you need to change it,
but now the types don't match, and those that
existed I found them in real-world code bases
that's why I mentioned the examples.
You, however, sir, I know who the … are
in a special situation because you own an
internal code base that is monolithic, has
great tools for it, where you can simply turn
a switch and do these things for people. Those
of us who don't have those tools would like
our compilers to help us with auto.
Male: I'm going to have to see like war story,
use cases of pulling this off without there
being wild unintended consequences.
Herb Sutter: All right. Thank you for that
question. As we go to lunch, let me ask this,
any of you who are listening to [inaudible
01:40:05] and thinking of, know war story,
please, tell him, and I will ask him after
lunch … Oh, there's … It looks like there's
going to be a line, but I would be curious
the answer myself after lunch, and let's get
back together.
Male: Absolutely.
Herb Sutter: Thank you for coming. Enjoy lunch.
See you back here before two o'clock. Thank
you.
