Cookies   I display ads to cover the expenses. See the privacy policy for more information. You can keep or reject the ads.

Video thumbnail
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.