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.
Herb recommends passing a shared ptr by value if you want to add to the ownership of it - what about a constructor?
In this example, does the 'ptr' cause an unwanted second refcount increment and then decrement? In the past, I've used a const ref of the shared pointer in the constructor parameters so that i can copy it to the object's members with only one copy.
Or does it get moved directly into m_ptr as opposed to copying?