We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Effective Modern C++

00:00

Formale Metadaten

Titel
Effective Modern C++
Serientitel
Anzahl der Teile
170
Autor
Lizenz
CC-Namensnennung - keine kommerzielle Nutzung - Weitergabe unter gleichen Bedingungen 3.0 Unported:
Sie dürfen das Werk bzw. den Inhalt zu jedem legalen und nicht-kommerziellen Zweck nutzen, verändern und in unveränderter oder veränderter Form vervielfältigen, verbreiten und öffentlich zugänglich machen, sofern Sie den Namen des Autors/Rechteinhabers in der von ihm festgelegten Weise nennen und das Werk bzw. diesen Inhalt auch in veränderter Form nur unter den Bedingungen dieser Lizenz weitergeben
Identifikatoren
Herausgeber
Erscheinungsjahr
Sprache

Inhaltliche Metadaten

Fachgebiet
Genre
Abstract
Scott Meyers’ Effective C++ books are renowned for their clear, insightful descriptions of how to get the most out of C++, but they were written for C++98—for “old” C++. “New” C++ is defined by the C++11 and nascent C++14 standards, and Scott Meyers’ forthcoming Effective Modern C++ is devoted to the effective use of features in C++11 and C++14. For this presentation, Scott will select a few guidelines from Effective Modern C++ and walk you through them. The guidelines will focus on specific practices in C++98 that require revision for the most effective use of the modern versions of C++.
SoundverarbeitungMAPProjektive EbeneBitFlächeninhaltURLAutomatische HandlungsplanungFormale SpracheAuswahlaxiomCASE <Informatik>FontSoundverarbeitungFolge <Mathematik>Güte der AnpassungComputeranimation
StatechartIterationObjekt <Kategorie>DatentypRechenschieberVariableTypentheorieÄhnlichkeitsgeometrieCodeSystemaufrufKonstruktor <Informatik>VektorraumTypentheorieIterationZeichenketteCASE <Informatik>InformationMatchingCodeLesen <Datenverarbeitung>QuellcodeAlgorithmusElement <Gruppentheorie>SchlussregelSpannweite <Stochastik>StandardabweichungKomponententestLambda-KalkülZahlenbereichDatenstrukturSpeicherverwaltungProdukt <Mathematik>GleichheitszeichenPunktTypinferenzDeklarative ProgrammierspracheProgrammbibliothekZweiMailing-ListeAnfangswertproblemEreignishorizontSoftware EngineeringDatenfeldImplementierungFlächeninhaltMathematikMaschinenspracheAbgeschlossene MengeFormale SpracheZweiunddreißig BitSystemplattformKonstruktor <Informatik>ResultanteCompilerProxy ServerParametersystemKonditionszahlBildschirmfensterBitDifferenteAutorisierungKlasse <Mathematik>MultiplikationsoperatorStellenringLokales MinimumRechter WinkelGanze ZahlMAPUmsetzung <Informatik>Sampler <Musikinstrument>SchlüsselverwaltungVariableInnerer PunktProgrammierumgebungVisualisierungEntscheidungstheorieSystemaufrufZeiger <Informatik>Objekt <Kategorie>URLFehlermeldungVersionsverwaltungVirtuelle MaschineVerkehrsinformationWort <Informatik>ProgrammfehlerUniformer RaumThreadSchreiben <Datenverarbeitung>DatenparallelitätArithmetischer AusdruckÄhnlichkeitsgeometrieBetriebsmittelverwaltungNeuroinformatikNichtlinearer OperatorComputerspielPoisson-KlammerGeradeAdressraumCachingBefehl <Informatik>FunktionalAuflösung <Mathematik>ValiditätQuick-SortMathematische LogikMereologieSoftwareentwicklerSchnelltasteTransaktionWechselseitiger AusschlussProgrammierungSynchronisierungWidgetInformationsspeicherungOrdinalzahlLaufzeitfehlerArithmetisches MittelMultiplikationKartesische KoordinatenTemplatePhasenumwandlungProgrammiergerätAlgebraisch abgeschlossener KörperGüte der AnpassungAggregatzustandMinkowski-MetrikKontextbezogenes SystemAusnahmebehandlungHalbleiterspeicherInstantiierungFitnessfunktionClientLuenberger-BeobachterLoopRohdatenTermAuswahlverfahrenFormale SemantikAutomatische HandlungsplanungGrenzschichtablösungInverser LimesFaktor <Algebra>AuswahlaxiomAutomatische IndexierungPerspektiveRauschenSelbstrepräsentationBoolesche AlgebraATMMultifunktionHyperbelverfahrenRichtungSchaltnetzFeuchteleitungFolge <Mathematik>Weg <Topologie>Gewicht <Ausgleichsrechnung>WiderspruchsfreiheitInterpretiererNichtunterscheidbarkeitCoxeter-GruppeEigentliche AbbildungProgrammverifikationDelisches ProblemArithmetische FolgeSummierbarkeitInformationsüberlastungDämpfungKonstanteCharakteristisches PolynomInhalt <Mathematik>Web SiteÜbersetzer <Informatik>ZustandsmaschineGoogolTeilbarkeitFigurierte ZahlSummengleichungKonfiguration <Informatik>Einfache GenauigkeitStreaming <Kommunikationstechnik>HochdruckForcingGrundsätze ordnungsmäßiger DatenverarbeitungSinusfunktionDesign by ContractSichtenkonzeptProjektive EbeneGruppenoperationPlastikkarteBildschirmmaskeWorkstation <Musikinstrument>Peer-to-Peer-NetzPhysikalischer EffektMessage-PassingDualitätstheorieRechenschieberMomentenproblemProzess <Informatik>Gesetz <Physik>SoundverarbeitungComputerarchitekturExtreme programmingOktaederDatenbankFormation <Mathematik>Lie-GruppeFokalpunktPhysikalisches SystemProgrammschleifeBitrateIndexberechnungInterface <Schaltung>SpeicherabzugKlassische PhysikPRINCE2MaschinenschreibenKurvenanpassungSoftwarewartungComputeranimation
Transkript: Englisch(automatisch erzeugt)
Well, good morning. I'm Scott Myers. I thought I'd be talking a little bit about C++ today. I am standing in the only location I can find on the entire stage where I can actually see you. This is relevant because if you want to ask questions and you raise your
hand, I'm probably not going to be able to see you. So this is what we're going to do. Normally what I tell people is if they don't understand something or they're a little concerned, they should ask a question, which I want to encourage. If you feel uncomfortable asking a question, what I normally encourage people to do is to look really confused and
to stare directly at me. But that's not going to work because I have lights in my eyes I won't be able to see you. So you have two choices. One of them is you can make lots of motion like this because I think like insects, I can in fact detect motion. So that's one possibility. Or if you see me, go to the only location on the stage where I can see you, that might help. Or you can start making really uncomfortable
sounds as if there's just something terribly wrong and then I'll try and figure out what's going on. So that's the plan for questions and answers. Also, if you don't want me to see you, you need to encourage me to go anyplace except that one location on the stage. I want to talk today about effective modern
C++, which is the latest project I've been working on to try to put together guidelines for how to make the most effective use of the language. And in this particular case, when I talk about the language, I'm talking about the new features in C++11 and C++14. So for purposes of the discussion today, I'm assuming you already know everything in C++98. That's old stuff. So the question is how do you make effective use of the
newer stuff? And the outline that I have for the work that I've been doing is to break down the general topic areas in C++11, C++14. This is what I've broken them down into. When I started the project, I did not plan to
talk about type deduction at all. I don't like talking about type deduction. It's kind of a dull topic. I found out I was unable to discuss C++11 and especially C++14 without explaining how type deduction works because type deduction is widespread in C++11. It is essentially much more widespread in C++14. That's a whole separate talk, and
in fact, I'll be giving a talk on that topic this afternoon at 4.20. That's a different talk. The other topic areas that I have been investigating, putting together guidelines is auto gets its own chapter, a whole bunch of information on changing the habits you have from C++98 to moving to C++11 and
C++14, different ways of doing things. The only things I'm going to be talking about today, because we have a limited amount of time, is I chose three guidelines that
I hope will be useful and give you some insight. They happen to come from the information on auto as well as the information on moving from C++11, excuse me, C++98 to C++11 and 14, and as it turns out in this particular presentation, I'm not talking about anything specific to C++14 at all. I'm only talking about C++11 stuff. It just kind of worked
out that way. And the first guideline I want to give you is to prefer auto to explicit type declarations. So fundamentally, you have a choice here. So if I want to create some widget W, I have a choice. I can say, OK, I want a widget called W by giving the type explicitly and then I initialize it with some expression of type widget. Or what I could do instead is I could say
auto W and then give an expression of type widget. In both cases, they will create a widget object. So what I'm encouraging you to do is to not type out widget expressly but instead to use auto, which leads to the question why am I advocating that? So we'll start with the little stuff first. The little stuff is that using auto saves
you some keystrokes, which is important from a typing perspective. It is in some sense more important from a reader's perspective because the more you can get rid of noise that's in your source code, the easier it is for people to understand basically what you are trying to accomplish. So if I have a vector of string iterators,
VSI, so it's just a vector of iterators into some strings, and I want to do something for every iterator in that container, what I could say is, OK, for every string iterator SI in the container VSI where I'm explicitly stating the type, or I can just say, listen,
for every iterator SI in the container, and I just use auto here, my feeling is in this particular case explicitly stating that it's a string iterator does not add any information that is not already present in the source code. Anybody reading this
has got to know this is a container of string iterators or they'll never understand the code. So I think auto makes it easier to understand and, of course, easier to type. Now, the easier to type thing normally is not a very compelling argument, but there are some cases where it can be helpful. So if I'm writing an algorithm taking a begin and an end iterator, and I decide what I want to do is I want to make a copy of the first element in the range,
and we'll make the assumption it's not an empty range. Well, if I have an iterator pointing to something, the question is what is the type of the thing that it refers to? And I know you all roll your eyes and you say, well, of course, it's type name, standard iterator traits of iterator colon colon value type. Who does not know that?
Actually, it turns out several people don't know that. And even if you do know it, I don't know how many people have said, you know, what I'm really looking forward today is typing this many, many times. That's not really productivity flowing through your fingers. Now, in this particular case, you can just say auto first element is star B.
This is very clear. B points to the first element of the range, so this is going to be a copy of it. So I think it's clearer for people to understand. Certainly it's easier to write. However, that's really a syntactic convenience. There are some more compelling technical reasons
for using auto. There are a number of cases in C++ where we think we know what the number is. We're not actually specific. And here's a common kind of mistake. So if I have a vector event and I call the size member function, now, it turns out that the return type
of calling size on a vector in this particular case, it's vector event colon colon size type, which is a typedef for some unsigned type. But the return type is supposed to be vector event colon colon size type. I don't know anybody who uses that. What most people do is
it's an unsigned, close enough for government work. It usually works. But as a specific example of a case where it can fail, so if you are on a 32-bit window system, this is completely safe code. There is nothing wrong with this. But the only reason there is nothing wrong
with the code in the sense that it will actually work under all conditions is that an unsigned and a vector event colon colon size type on that platform with that compiler are both 32 bits. So they're the same thing. It doesn't matter. But if you go from 32-bit windows to 64-bit windows, suddenly things change. Because on a 64-bit windows platform, an unsigned is 32 bits, but a vector event colon colon size type
is 64 bits, which means as long as the number of elements in the container is, oh, 4 billion or fewer, it will work. But if it exceeds 4 billion, you've got a problem. Now, how many people when writing their unit tests are testing with containers of more
than 4 billion elements? Oh, let me go to my spot where I can see you. I'm not seeing too many. I'm seeing no hands. This is the kind of mistake that can creep in because you're not actually using the right type. If, on the other hand, you said auto of size is v.size, you automatically get the right type on the right platform.
So now we're moving beyond simply it's easier to type. Now we're getting to some actual technical reasons. I have a map where the keys are strings and the values are integers. And now I decide what I want to do is some operation, it's a read-only operation,
on every one of the elements of that map. So it would be common for people to say, all right, for every reference to a constant pair from string to int, because I know that a map holds pairs, the key is a string, the value is an int,
it's a read-only operation I want to perform, so I don't want to copy that pair, so I'm going to have it by reference to const. So this says, for every one of those pairs in the map, do something with it. It seems entirely reasonable. This will compile.
Now, this will run. This will run slowly. And the reason this will run slowly is because although this is a map from string to int, you may dimly recall that the key part of a map is declared const. So the contents of a map are actually const strings and ints.
That's the type of pair stored in the map. It's a pair of a const string and an int. What this code says is take every one of those pairs consisting of a const string and an int,
from it, copy it into an unnamed temporary pair of type string and int. So you just paid copy everything. Bind that temporary to the reference to const and do something on the
temporary. Which is probably not what you wanted to do. Now, issue number one, performance problem. You're copying everything even though you specifically pass things by reference to const to avoid copying them. But there's some more subtle issues here, too.
Inside the loop here, which I'm not showing, but inside the body of the loop, if you are assuming that you actually have a reference to a pair in the map, you have a reference to an element of that map, you're assuming, for example, you could take a pointer to it and get a pointer to a pair in the map.
But with the code the way it's written here, if you take a pointer to it, you will take a pointer to the temporary copy of what's in the map. And the validity of that pointer will go away at the end of the loop iteration, you will be left with a dangling pointer. So if you stored that pointer someplace, maybe in another data structure, thinking that its lifetime was valid as long as the elements of the vector,
you have a really fun way to spend the dark Norwegian winters debugging your code. However, if instead you decided to use auto, because that's what the cool kids do,
you could just say const autoref, and this will automatically pick up the appropriate type that is stored in the map. It's still stored by reference to const. This won't generate any temporaries at all. This will actually give you a reference to the elements of the container. Any questions about how this works?
So the question is, what type would be deduced for auto here, essentially? And the answer is, auto will deduce the actual type of what is stored in the container.
So it's going to be a pair of const, string, and int. That's what auto will expand into. At this point, I hope you are beginning to appreciate that auto is not a matter of making things easy for people to type, although that is part of it. That's sort of the initial allure.
There's more to it. There's performance issues, there's correctness issues. Auto simply helps you for making certain kinds of mistakes. Let us suppose you create a function object. Probably the easiest way to create a function object is by using a lambda expression. If you create a lambda object, create a closure,
technically, and you want to store it in a runtime data structure so you can use it later. Maybe, for example, you have a container of callbacks, so when an event occurs, you want to call some arbitrary number of functions. You need to be able to store the lambda somewhere. You can store it using a standard function object here.
This is actually an object of a template which will store the closure created by the lambda. This will work fine. However, a standard function object is essentially fixed in size. During compilation, the compiler sets aside a certain amount of space for a standard function
object because it's just a class instance. It uses a certain amount of memory. So the question is, what happens if the closure that is created from this lambda, what if the closure won't fit in that chunk of memory? Because it might not. It might be too big. No problem.
Standard function under those conditions will allocate heap memory and store the copy of the closure on the heap which leads to the observation that the closure might be stored on the heap. So, using this, depending on the size of the closure created from the lambda, which is determined by the number of objects that are captured by the lambda,
you might pay for heap allocation. Of course, you'll pay for heap deallocation later. If I now take the resulting object, this is f1a, so this is a function object. If I now take this function object, so I can call it, and I now say fla of 22,
this is an invocation of the function object. I'm calling the function object. Now, internally, the function object might hold a pointer which points off to something on the heap which is ultimately what's going to get invoked. The reason that's important is because it means that this function call here possibly is an inline function call but possibly is an
out of line function call. Maybe it's in line, maybe it's not. Depends on a combination of types. Or if you take exactly the same lambda and instead store it in an auto object,
the compiler will figure out the type corresponding to this lambda, whatever that type is, the compiler figures it out. Once the compiler knows what the type is, it knows exactly how much space is needed to store an object of that type. It will therefore take an object of that type and it will put it on the stack in an object. This will not be stored on the heap
under any conditions. So this is definitely not on the heap, and this is not an optimization issue. It's just not on the heap. And when I now make the function call here, I'm going to say the closure call is typically in line. In fact, I would assume it is going
to be in line. The only reason I say typically is because there's some language lawyers in this room and I have to watch out because the standard is not 100% guaranteed that it's going to be in line. Having said that, it's going to be in line. Maybe not in debug mode. So we end up with a
situation with using a standard function object unknown type. We possibly incur heap allocation and deallocation. We possibly give up in lining. Typing auto instead gives us definitely no heap allocation and deallocation and also essentially guarantees us in lining. In this case here, I'm showing a function object created from a lambda. There are other ways to create
function objects, so if you are one of the people who likes to use bind, and Nico's talk notwithstanding, there are some people who do like to use bind. Most are part of a 12-step program to eventually get them away from it. But there are still a few people going through that process, and, in fairness, there's a lot of legacy code that uses
bind because bind's a perfectly valid tool prior to C++11. So this idea of using auto to store function objects, it's not a lambda-specific thing. It's valid for any function object. So I have encouraged you to use auto instead of using explicit type declarations.
It's great advice, except when it doesn't work, because sometimes auto does the wrong thing, which means we need to have a better understanding given the technical advantage we've talked about of when auto does the wrong thing and what to do about it. So the first thing has to do
with braced initialisers. If I say auto, give a variable name, and then I give a braced initialiser. Special rule in the standard. This deduces a type of standard initialiser list
of int. Now, if I only show you that piece of information, you might go, well, all right, that doesn't seem too bad. The problem is if I say auto x2 gets five, it deduces a type of int. If I say auto x3 parenthesis five, it deduces a type of int. So if I say auto and
a type like an int, some syntaxes deduce the type of int. But if you use braces, it will deduce a different type. In particular, if I say braces like this with or without the equal sign, it makes no difference. They both deduce and initialise their list of int.
How many people have made this mistake in their code? Oh, you guys have not lived. It is a veritable rite of passage when moving to C++11 to start typing auto and then suddenly find out that nothing is working the way that you expect, only to find out it's because you
thought you were creating an int here and you actually created an initialiser list. It's only the case for braces. Fundamentally, if I say auto x and then I have a braced initialiser, the equal sign is optional. It does not matter whether the equal sign is there. It's the braces that are important. Basically, under these conditions, the type of the
variable you are creating is not going to be the type of the expression. So decl type of x, the type of the variable you are creating is not going to be equal to the decl type of expression, the type of the expression you are using to initialise it, which is a little strange because normally you say auto deduces the type of the initialising expression.
This is the one case where it does not. In this syntax here where I have auto and I have braces, that's the combination that leads to trouble, auto and a braced initialiser. Again, the equal sign doesn't make a difference. When you have that combination,
the type of the variable you are creating is more or less equal to an initialiser list of the type of the expression. The reason I say more or less equal is because if I showed you what it was equal to, it would keep going over that direction for a ways. Frankly, what it is equal to is not anywhere near as important as understanding that it doesn't deduce the type
that you expect. The comment is that the curly braces initialises that it's supposed to be an array. The curly braces are used to mean more than one thing is the problem.
In this particular context, it would be perfectly reasonable to say it's trying to interpret it as a sequence of values. C++ does not apply that interpretation consistently. It's kind of a bigger story. Regardless of whether it is applied consistently or not,
what I will say is that this is literally the only place in C++ where a braced initialiser will be deduced to have this type. As an example, if you take a braced initialiser and you pass it to a template, type deduction will fail. A braced initialiser does not have
a type. It's just a special rule for auto. So many people have tried to say, okay, this is how I'm going to think about it, and there are ways to think about it that can help reduce the confusion, but as far as I know, there is no single interpretation which is consistent across the entire language. Another problem for type deduction,
for auto type deduction is hidden proxy types. I'm going to talk about vector bool. If you've had a proper education in C++, when you see vector bool, you begin to feel nauseous and because you've been taught you should never use it or possibly you shirk in fear.
The only reason I'm using vector bool is because it's part of the standard library, which means you can verify everything I'm going to tell you by going to your standard library implementation and looking inside the code to see exactly how it's implemented. This is a representative of a large class of libraries that use what are known as proxy types.
I only choose this one because you can check this one out yourself, and I'll get back to that in a moment. So if I have a vector of bool, a vector of bool is a packed representation of bools. Every Boolean takes a bit. So if I now say, okay, bool B1 is VB sub 5. In this case here, the type of B1 is bool. Unsurprising. I've declared
it to be bool, so B1's type is bool. This is not a surprise. If I say auto B2 gets VB sub 5, we have a problem. The reason we have a problem is because VB sub 5, the indexing operator for a
vector, the indexing operator for a vector normally returns a reference to the indexed element. If I say VB sub 3, I normally get a reference to the third element. You can't have a reference to a bit. Smallest addressable thing in C++ is a char.
You can't have a reference to a bit, but this is a vector of bits, basically. So the question is, how did they make that work? The way that it works is because VB sub 5 for vector of bool
returns an object of type vector of bool colon colon reference, which is actually a class. Don't be fooled by the name. It's a class. It just happens to be called reference. When I say auto B2, the type I get back is vector of bool colon colon reference. This code compiles, but you don't have the type you think you have. In many, many cases, you'll continue to
use the object. It will behave exactly the way you think it's going to behave because there's an implicit conversion from vector of bool colon colon reference to bool. If you ever try to use this thing in a Boolean context, it converts and life goes on. Almost like they designed it that way. But the fact that the return type of the array bracket operator on a vector of bool
is not a reference, you can tell from time to time. For example, if I were to assert that the address of B1 is not equal to the address of B2, which makes sense, local variable B1, local variable B2, they had better not have the same address. If they had the same address,
the compiler was doing something very peculiar. So I should be able to assert that they are not at the same location on the stack. That assertion will not compile. The reason the assertion won't compile is because you are attempting to compare a pointer to a bool with a pointer to a vector of bool colon colon reference and you can't compare two different
pointer types that have nothing in common, so the code won't compile. Think about proxies. Proxy objects are designed to stand for something else. So a vector of bool colon colon reference
object is designed to stand for a bool. And there are some assumptions made usually by implementers regarding the lifetime of those temporaries. And in particular, a common assumption is that they're not going to live beyond the end of the statement. As an example, in GCC 4.7, probably still true in 4.9, I just haven't checked. I don't check every time a new version
of the compiler comes out. In GCC 4.7, vector of bool colon colon reference, that type, and you can consult the library to find out what it does. That type contains a pointer to a word on the machine with the referenced bit. Remember, it stands for bit. So what you actually get back is an object which has a pointer. The pointer points to the word and
it's got an offset which tells it which bit in the word is the one that it points to. So a vector of bool colon colon reference is a pointer and an index that says this word, this location in the word. That's how it's implemented on that particular library. At least it was in GCC 4.7. Now that means we have a pointer to a chunk of memory and that leads
to the possibility that we can have a dangling pointer if the lifetimes aren't correct. I have an example. So here's a function called make bool, it's a factory function, it returns a vector of bool. So this is a function, creates a vector of bool, returns it
as its return value. Now what I do is I say make bool, that's the function call, so make bool gives me this return value which is a vector of bool. I index it to get element number three which we will assume is a valid element. So now I have one of these vector of bool colon colon reference objects.
It contains, in GCC's case, a pointer and an index. But I've declared the result type to be of type bool. So there's an implicit conversion from a vector of bool colon colon reference into a bool. So what it does is takes the pointer, goes to the word, finds the bit, sees if it's true or false, sticks the value in B1, life's wonderful.
Everything works. Instead, if I say, okay, auto bit is make bool vec sub three, again call make bool vec, get a temporary vector of bool, index it to get element number three which we will assume exists, get back a proxy object,
which we now store. So now I've got this proxy object called bit. It contains a pointer to the vector of bool that was returned by this function. That would be the vector of bool that will be automatically destroyed at the end of the
statement because it's a function's return value. Which means by the time we finish executing this semicolon here, we have this charming object on the stack which contains a pointer to memory which no longer corresponds to a vector of bool. And now I just say, okay, B2 is of
type bool and it's a copy of bit. This has undefined behavior. Now, these two statements appear conceptually to do the same thing, but they don't do the same thing. So this is an wrong type from what we want. As I said, the reason that I chose
vector of bool is because you can verify everything. I'm telling you by looking at your library implementation to see exactly what they do with vector of bool colon colon reference. Have a field day. I'm not lying to you. But you're probably not using vector of bool.
But if you are using other third party libraries, especially in areas where you're trying to get maximum performance, which turns out to not be terribly uncommon for C++ developers, you could well be using a library that is using proxy types. A while ago I took a look at the
boost library. So this is an example of some boost libraries. All of those libraries use proxies. How do I know? They document it. I do not know of an easy way to avoid having auto deduce a proxy type. It, alas, requires some knowledge of the library that
you are using. Essentially what's happening is when you use auto with those kinds of libraries, it is deducing an implementation detail that the library author had hoped to keep hidden from you. And you need to be aware that if you use auto, there is a possibility that you're going to have that problem. So if you know you're using this kind of a library and the
only way I know of to know that is to simply read some documentation about the library, you need to use auto with greater caution. Doesn't mean you don't use it, just means you need to be aware that you don't want to use it probably all the time. And for what it's worth, there actually have been some proposals for C++ 17 or maybe C++ 20 or C++ 23. Maybe they'll fix it, but it doesn't do us any good right now.
So for the foreseeable future, you just need to be aware of this problem. When I talk about auto, sometimes people are very resistant to the idea and they go, you know, I'm not going to be able to understand the code and I'm very concerned about having maximum code clarity. This is fine. So if you believe in your professional software
engineering judgment as engineers, that if the explicit type is clearer, if it will yield clearer, easier to understand, easier to maintain code, great. That's what they pay you for, is to use that engineering judgment. Use it. But I do think it is important to bear in mind that first there's been a ton of success in other languages with similar features.
C++ is not at the cutting edge of type inference. C++ is at the leading edge of type inference. This caused me the trailing end of type inference. So this is not new territory.
And I also remark as a practical matter, a lot of people using IDEs, for example, Visual Studio, the type of an auto-deduced variable is actually visible in the IDE. So although it may not be expressly visible in the source code, it could be visible through a tool that you are using in your developing your code. So I think you should bear that in mind when making the decision whether to use auto or not use auto. So my guidelines are to prefer auto to explicit
type declarations, which I do think yields, generally speaking, easier to write, easier to read code in many cases and avoid certain kinds of tricky errors involving either performance or correctness, as we have seen. And certainly you should definitely remember that auto plus
a brace initializer will always deduce an initializer list type. Any questions on that? I'm sorry, time's up. New guideline. I want to talk about parentheses versus braces, which builds a little bit on Nico's talk last time. My guideline, and I spent literally years trying to figure out what is my advice
about parentheses versus braces. And my advice is understand the tradeoffs, because I don't think there is a great piece of advice for what to do. We start with the notion of uniform initialization. Uniform initialization. It's uniform. We can use it everywhere. Braces,
it's great. So if we want to initialize an integer with 44, this will work. Remember, don't use auto. If I have a struct and I want to initialize the fields of the struct, I can use braces. Great. If I have a string and I want to give it a value, I can use braces. So wonderful. If I have a class, it has a data member, I can use braces on the member
initialization list. This is so cool. If I have an array, I can use braces. Somebody mentioned earlier, it's supposed to have values of an array, but that's not the only place. If I have a vector, I can use braces. If I have a raw freaking pointer, I can use braces to initialize its value. Not that anybody would use pointers.
Uniform initialization. That's the sales pitch. You can use it everywhere. I don't use the term uniform initialization, and the reason I don't use the uniform initialization is because that suggests it's uniform. You should use it everywhere, and I don't believe that is reasonable advice. I think there's a lot of advantages to what's
known as uniform initialization. However, I prefer to refer to it as brace initialization. That's what I'll be calling it from now on. So, first thing about brace initialization is it has the unique feature which is that narrowing conversions are illegal. Only brace initialization will reject narrowing conversions. A narrowing conversion
basically means that the compiler cannot guarantee that the value of a larger type will be able to be stored in a smaller type. So, as an example, if I have a point which has two integers, and I say point P1, curly brace 1, 2.5. Now, in C++98,
this was completely fine code. The 2.5, which is a double, would be truncated to initialize Y. So, in C++98, that compiles, has well-defined semantics. In C++11, this code will not compile, and the reason it won't compile is because I'm using braces, and this is a double, and that's an int, and 2.5, last I checked, cannot be exactly
represented as an integer. Code won't compile. So, if you want it to compile, you can make it compile, you just do a cast. All the cool kids use static casts. C casts.
But it's important to understand this notion of rejecting narrowing conversions exists in exactly one place in the language, only brace initialization. So, if I say int A is 2.5, this used to compile because it goes back to the days of C, and it still compiles. It will truncate the 2 and initialize your integer. No warning, no error. Might get a warning.
But if you try to do exactly the same thing using braces, the code will be rejected. Given that narrowing conversions normally don't make any sense, like I want to store a double as an int, an implicit truncation, I think it's a feature that braces don't let you do that. That's a plus. We now have two different syntaxes for calling constructors, so I can say widget
W1 and I pass some arguments in curly braces, widget W2 and I pass arguments in parenthesis. They both work, usually. Both choose the best match constructor, however, there are different
rules for what it means to be the best match. Now, this is something that Nico talked about in his last presentation. Only brace initialization matches initializer list parameters, and those matches are actually preferred. Let me give you an example. This is a simplified declaration from the standard library from the standard vector class.
Here's vector. It takes a number of elements and a value. It takes an initializer list of T. This is not affected by the change that Nico was talking about at the end of his last talk. So, if I say V1 with 100 comma 5, notice that I'm using braces. Now, because I'm using braces,
I could pass the 100 is the size and the 5 is the initial value, two arguments, that would work. Or I could say 105 are two initial values to go into the vector. There is, in some sense, ambiguity here. But the language rules say, when you use braces, and it is possible to call an initializer list constructor, that is the one that you call.
It's not ambiguous. So, this means that the size of the vector is two and the values are 105. It calls this second constructor. If I take exactly the same code and I say, you know,
I'm a traditionalist. I like the old ways. And the old ways of passing constructor arguments was to use parentheses. So, exactly the same code, but now I use parentheses. Under these conditions, now it calls the first constructor because initializer list constructors are only considered if you use braces. If you don't use braces, they're not even considered. So, the result of this is a vector whose size is 100 and values is set to
five. All of them are five. So, it does something different. They're not interchangeable. Now, this only makes a difference if you actually have an initializer list constructor.
Here's a gadget class. It has one constructor. It takes two integers. I want to create a gadget G1 with 10 and 20. Braces and parentheses, they do exactly the same thing. They both call this constructor. Makes no difference. They do the same thing.
If I now say G3 with 89.5 and zero, notice I'm using parentheses. I'm going to pass it to an integer. No problem. I'll take that 89.5, get rid of that pesky .5. We didn't want that half of a number anyway. Compiles. But if I do the same thing inside curly braces, it won't compile. And the reason it won't compile is because this double can't be
expressed in that integer exactly. Again, you get brace initialization rejects narrowing conversions. Does this also apply to larger integer types going to smaller integer types?
Correct. It does. Essentially the way it is is the only time that if you have a larger type going to a smaller type, the only time that that will be permitted to compile is if the compiler can prove that it will fit. As an example, if I said I want to initialize an int and I said
the initial value is going to be 22 long. So long is bigger than an int in general. But we know that the value 22 will fit in an int. That will compile. On the other hand, if I said long l equals 22 and initialize the int with the long in curly braces, that won't
compile. Make sense? So basically it doesn't compile unless the compiler can guarantee it's okay. Was there a question? Is your question what would happen here if we did not have
the initializer list constructor? Okay. If we didn't have the initializer list constructor,
they'd both call the first constructor. They do exactly the same thing. That's essentially the same as this example here. All right. So now this leads to some really interesting situations. So I got tired of using widgets. I've been talking about widgets for 20 years.
I'm moving on. I'm progressing. I'm growing. Let's talk about thingies. So I got a thingie that takes an int and an int and thingies take an initializer list of double. So if I say parentheses with 10 and 20, it calls the first one. Remember, with parentheses, it will never call an initializer list constructor. It's very simple. It won't call it.
If I use braces here, notice that these are integers. And x and y are integers. And this is an initializer list of double. It's going to be a better match to go from two ints converted to doubles than two ints calling x and y. If there is a way to make the initializer list constructor work with braces,
that's what will get called. If I have T3, 89.5 and 0, this is a matter of type conversion, so this will call number 1. Notice that I've got a double here. This is a double.
It still won't try to be converted into an initializer list. If you are using parentheses, the compiler literally pretends as if that constructor, the one with the initializer list, did not exist.
All right. Now, this means that we can get some really interesting implications if we add new constructors to classes. Now, I want to be clear here. Any time you have a class with some interface and you add a new overload to that class, it is possible that existing code that used to call some function in your class
may now start calling the other overload. That's inherent when you add a new overload. So there's nothing special about initializer lists here. If you add a new overload to an overload set, code that used to compile and call one overload
might now call the new overload, which is probably why you added it. So in this case here, I've got a widget. I'm using braces because I've embraced the uniform initialization lifestyle. This calls widget with 10 and 20. Life goes on. Life's good. Somebody comes along later, they add an initializer list constructor to it.
Notice it's an initializer list of float. This code, unchanged, legacy code, no longer calls this constructor. It now gets converted to two floats because braces really want to match an initializer list constructor. If there is any way to make it compile, it will compile.
This is neither a good thing nor a bad thing. It's just a characteristic of the way overload resolution works in C++ and you need to be aware of it. Now, presumably, when you added this initializer list overload, you said any time anybody is using braces, I want to treat that as an initializer list.
That's not what you meant. You need to have a little talk with the API designers. So here's our situation. Essentially we've talked about this here. The guideline is to distinguish between parentheses and braces when initializing objects. Any questions about that?
The last guideline I want to talk about then is to make const member functions thread-safe. We have known from the beginning of C++ that there were two different ways to interpret const
ness. There's logical const ness, which means conceptually nothing changes, and there's bit-wise const ness, which means none of the bits inside an object actually gets modified. So bit-wise const ness, this is what compilers enforce. Conceptual const ness, this is what developers should implement. With any luck, this is completely old news. We've been talking about this since 1995.
I've been talking about this since 1995. My God, what happened to me? Never mind. All right. Mutable. Also available since 1998. Mutable, as I like to put it, tells the compiler, ha ha, I was kidding about the const.
So a mutable data member is permitted to be modified even inside a const member function. It is very useful when implementing conceptual const ness, in particular, for doing things like caching, which is probably its most common use. So here I have a widget class. It has a member function called magic value,
which computes a magic value. So computing the magic value conceptually does not change the widget. If I go to a widget and say, so what's your magic value, it goes crunch, crunch, crunch, crunch. Here's my magic value, but it didn't change the widget. It's the same widget it was before. If, however, we assume that it's computationally expensive to determine what the magic value
is, we would prefer not to have to compute it unless somebody asks for it, which means we don't want to compute it up front. If somebody does ask us for it, we don't want to have to compute it multiple times, because it's expensive, which means we'd like to cache it if we ever do compute it. So we can implement it like this. We can say, all right, I'm going to create a mutable bool called cache valid and a mutable int cached value.
What I'm going to do is if I ever compute the value of the magic number, I will store it, and then I will say that the cached value is valid. So I can look it up again in the future, save myself some time. So here's my code. If the cache is valid, great, I just return the cached value.
Now, if the cache is not valid, maybe because nobody's ever called this function before, then what I do is I say, okay, the cached value is I call some expensive computation, I say the cached valid is now true, and I return the cached value. Perfectly standard way of implementing a cache using mutable.
In C++98, there's no C++11 here. Works fine. Life's good. C++ introduced this tiny new feature. It's called concurrency.
It has some minor implications for programmers. For example, nothing works anymore. A few things work. But as an example, let us suppose that I've got some widget w, and now in thread one, I say auto v1 is w dot magic value. Crunch, crunch, crunch, crunch. It's figuring out the magic value.
Thread two at the same time calls w dot magic value. Crunch, crunch, crunch, crunch. It's doing the same thing. Now, I want to go back a slide and show you that magic value, I have declared to be a const member function. It does not change the conceptual state of the widget. You should be able to have multiple people reading a widget simultaneously.
They're all readers. And because I declared it const, because it is logically const, I've declared these two data members mutable. The problem I have is that in this case here, here's thread number one, it's calling a const member function. It's a read operation.
Here's thread number two, it's calling a const member function. It's a read operation. If I know that I'm writing code with multiple threads simultaneously reading something, that does not require synchronization. I don't need to get a mutex or anything else if all my threads are readers.
It's safe to read things simultaneously. It's conceptually safe. The problem is that magic value, in case you've forgotten, that was the previous slide, those look like write operations to me. The const member function is conceptually const. It fulfills the conceptual const requirement.
The problem is it's not bitwise const, and that makes a big difference when it comes to currency. So this means both of the reads may actually write the mutable data members, which means we have two threads which are both readers and writers. That's the definition of a data race, that means undefined behavior, and that's just bad.
What's key here is there's nothing wrong with the calling code. Both threads are performing read operations. There's nothing wrong with those threads. They're doing perfectly reasonable behavior. Possibly the code was written so they would do a lot of reads. You can't blame them.
If the client code is not wrong, and the behavior is undefined, that pretty much means the implementation code is wrong. No other way to look at that. Widget magic value is a broken function. Now, fixing it is really easy, and this is where C++11 comes in. All we have to do is I'm going to slap in a mutex,
and I'm going to declare the mutex to be mutable because locking a mutex changes its state. It's a non-const operation. I need to perform non-const operations on the mutable inside the const member function. Sorry, on the mutex.
No problem. I lock the mutex at the beginning of the function. I perform the operation to figure out what the value is, and then this object in its destructor will automatically unlock the mutex, so this close curly brace here unlocks the mutex.
This works fine. This fixes the problem. Now, in some cases, you don't need a mutex. In some cases, you might only need an atomic variable, which is a variable where operations on it are guaranteed to be viewed atomically by other threads.
For example, let's suppose I'm keeping track of how many times the function is called. So here's my gadget class, here's a get weight function, and for whatever reason, I want to know how often is get weight called. Under those conditions, I could have an int and I could have a mutex to limit access to the int,
or what I can do, which is a little faster in many cases, is I can just use an atomic unsigned. So what that means is that when I call plus plus call count, that's a read-modify-write operation. It's guaranteed to be executed atomically, typically through a single underlying machine instruction. Typically, it is faster at runtime than using a mutex.
So this works fine. So notice I'm not saying you should always use a mutex. This doesn't need a mutex. However, if we go back to our example with the cache, and we say, okay, I'm going to make the cached value an atomic int,
and the cache valid an atomic bool, this is the way of thinking this as well. If atomic is cheaper than using a mutex, I'm going to use them all over the place. I'm a C++ programmer. I don't pay for what I don't need. So here's the code again. Now, in this case, I've rewritten it a little bit differently here.
So I check the cache to see if it's valid. If so, I return cache value. Otherwise, in this case, I perform an expensive computation number one, which I store as v1. I now perform an expensive computation number two, which I store as v2. I now say, okay, the cache is valid. That's true now. And I return the cached value,
which is v1 plus v2. So I've just broken the expensive computation into two pieces. That's all I've done. This is not thread safe. Why not? Let me get in my spot. Okay, now I can see you. Yes. Okay, you are correct. Each of the lines is atomic,
but the whole thing is not transactional. The problem is that one thread can come in, see that cache is not valid, start doing the expensive computations, and then can say cache valid is set to true. In the meantime, another thread comes in and goes,
oh, the cache is valid, because that other thread just set it to true. So it now returns the cached value, but we haven't computed the cached value yet, because the first thread hasn't yet performed the sum operation. Oops, wrong answer. Only happens once every five years or so, so we don't really care. We can fix that operation,
you say, no problem. What we'll do is we'll compute the cached value before we set cache valid to true. Right? That'll fix the problem. It will fix the problem. You'll get the answer. This works. It just works harder than it should.
Thread comes in, checks to see if cache valid is true. It's not. Starts performing the expensive computations and then adds V1 and V2 together. At this point, we've performed the expensive computations. We are ready to go. Another thread comes in, checks to see if cache is valid. It's not yet, because we haven't yet executed this statement
in the first thread. Enters the loop, starts those expensive computations all over again. In the meantime, 22 other threads also check cache valid simultaneously, because you are running on a 48-core machine, and they are now all performing the expensive computations. You are really going to get the answer now. Yes.
So the question is, won't each thread get its own value? If we proceed on the assumption that there is only one magic value for the widget, in other words, no matter how many times you compute it, it will always get the right answer. Every thread should ultimately get the same answer. It's just that we are going to compute those subcomputations every time.
Let me think for a minute. Let me put it this way. That's the best case scenario. Let me check and see. In this particular case, because they are both atomic, I don't have to worry about partial results being read by threads, so I believe that what I said is true.
But even if this does return the right answer all the time, it's much more expensive than it needs to be. So, you know, the problem is, if you have more than one data member, and they need to be kept in sync somehow, you need a transaction. If you need a transaction, almost certainly what you want to do is use a mutex, so declare the mutex as I showed you
before. Now cached value and cache valid, these don't need to be atomic any longer. Now, atomics are more expensive than non-atomics. Cheaper than mutexes, but more expensive than non-atomics. If you have a const member function in C++ 11,
the first version of the language that supports concurrency, it should either be bitwise const or it should be internally synchronised, meaning that if it's doing read and write operations,
outside callers can call it safely without having to acquire a mutex. If you find yourself in a situation where you know for a fact that your class will never be used concurrently, you just know that. The simplest way to know it, you're writing a single
threaded application. There's no other threads. Under those conditions, then using atomics or acquiring and releasing a mutex is incurring a cost you do not need. If the logic of your
that there will never be for a particular kind of object two threads using it simultaneously. For example, if I have a phased design and in the first phase of the design only one thread uses the object and in the second phase only some other thread uses the object but they never access the object simultaneously, if you know that, if by design of your program you guarantee
that they will never be simultaneously accessed, under those conditions, that's an exception to rule. So if you are writing purely single threaded applications, you don't consider think at all about threading, you can forget about this guideline, programming C++98. But if you are programming a context where it is possible for your classes to be used
in a multithreaded environment where there could be simultaneous readers, then you need to follow the guideline that I'm giving you, which is to make const member functions thread safe. Any questions about that? So the question is how do you ensure
that if you are using a third party library that they have basically done this. If you find a third party library that's not doing this correctly, you need to file a bug report with the library. It's a concurrency bug in the library. So you may have to pay
the cost of discovering that as a bug. Essentially in moving, in going from a world with only one thread to a world with multiple threads, it is not going to be surprising if third party
libraries are going to have some concurrency errors in them. Yeah. So the question is can't I move the mutex inside the else clause, and the answer is no, because then I'd be
reading cache valid outside the mutex. Okay, so essentially the question is all right, I want to move the lock guard inside the else, and then I'm going to make cache valid atomic so that my threads, trust me, you're still going to get in trouble.
In particular, on relaxed memory architectures, this leads us right down the road into memory barriers and stuff like that, and that is a hell I do not want to go to. So I'm afraid I'm going to have to cut you off if you ask any more questions about that. I have a laser. I'm not afraid to use it. Okay. So here's the summary of what I want to talk about. We talked about three guidelines, but I slipped an extra one in there.
So prefer auto to explicit type declarations. It's not just about programming style. It's also about efficiency and correctness. Remember that auto plus a braced initializer will deduce an initializer list. Distinguish parentheses and braces when creating objects. I'd love to be able to say always use one or always use the other. It's not that simple. I'm sorry.
Make const member functions thread safe. If you're looking for more information about this, there's a true work of literary genius in the works, I hope. I'm working on a hope it's going to be out in October. If you want other guidelines based on information
that will probably be in the book, I gave a presentation last year called an effective C++ 1114 sampler. It covers different guidelines than the one I talked about here. Herb Sutter has written a thing called elements of modern C++ style.