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

Video thumbnail
- My name is Nicolai Josuttis
and I wanna talk a little bit about C++.
You might have heard my name;
I have written a couple of books,
and I'm involved into C standardization
since the first standard, so for more than 20 years now.
So it's all my fault.
(audience chuckles)
When we talk about C++, I have problem.
I'm not the smartest guy, that's why I write books,
so I have a book where I can find out details,
and C++ gets more and more complicated.
This is a story about why and how things sometimes go worse
when we think they go better,
and how we should deal with that.
So, this is a talk about initialization.
Initialization is a topic we had since the beginning.
In fact, we had it even before the beginning of C++:
we started with C because we are backwards-compatible
with C, and we had initialization rules adopted from C.
So, we adopted that, and thus an int that is declared
on the stack has uninitialized value,
and we had adopted the usual way to initialize an int
is an equal sign, which is not an assignment operator.
Well, the way when we have aggregates, or structures,
or arrays, to initialize a value was
using an equal sign and curly braces.
So, we adopted these rules and we introduced classes,
and classes got a new syntax,
and the syntax was using parentheses,
because we had to support multiple parameters,
so let's introduce something new, parentheses,
and it turned out that we adopted the syntax
for the existing fundamental data types,
so we could use them to initialize an int in different ways,
either directly or with the expression type
and then the empty parentheses to create an int
and guarantee default or guarantee
an initialized value there,
which was especially important for templates.
Then, we had, okay, the problem that the containers
we couldn't initialize.
So, that was roughly the situation when we thought
we have to do something with this.
Oh, let's make it better,
and one thing we should make better
is to harmonize things, because for everything you see here
on the slide could be a type theft.
If you see a type xy and you don't know
whether it's a fundamental data type
or whether it's an aggregate or whether it's a class,
you have no clue how to initialize it,
unless you know the details.
We have another problem with this existing category:
we have different ways to initialize,
different terminology, and different rules.
Everything, every little piece was a little bit different,
and therefore, we introduced about seven primary terms
about initialization in the standard.
I don't explain them here.
So, different rules, different terms, different syntaxes.
Let's make it uniform, but, of course,
we have to be backward-compatible.
So, the decision was to take the curly braces
as uniform way to initialize.
Which means, of course, being backward-compatible,
we exist the existing ways to initialize an int
and we got a couple new ways to initialize an int,
so, direct initialization with or without int value,
and copy initialization, which is the name
for initialization with an equal sign,
which, again, might have a value or not.
Is that all we introduced with C++11?
No, we introduced even more.
We, for example, introduced a way to say...
Oh, by the way, first of all, let's discuss
that we also introduced this syntax
for all the areas we had.
In all the areas we had, whether we had aggregates
or whether we had fundamental data types
or whether we had classes, we are now allowed
to use curly braces with and without the equal sign.
And of course, we are backward-compatible.
This also applies to containers now.
We have introduced the technology
to say we can initialize a vector
by a certain number of values
which are literals or which are non-literals.
The way this helps this, we could, instead of creating
a multimap, the way that we insert make_pairs,
we could just declare what we wanna have here.
So, that's the story.
How many ways do we have to initialize an int?
Well, we certainly have these four,
which we got from both C++ and C.
Then, we have the new ways to use curly braces,
but then there's more: we also can initialize an int
by using auto
so that the type is also somehow deduced
from the initial value, and there, again,
we have different syntaxes
with some interesting consequences
which we have to discuss in a moment.
Then, well, while I was preparing this talk,
no, let me say it that way,
while I was starting to think about this mess,
I thought, "Well, is there even more?
"Oh yes, let's try out parentheses."
So, this works, but it's something different.
This, does this work?
No, that's a compile-time error.
Hey, Pablo, you should know.
(chuckles) You were on the committee.
(audience laughs)
So, does this compile?
Well, of course also not, you think,
but yes, it compiles.
Because this uses the comma operator
to initialize the int with the nine.
And does this compile?
No, there's an error.
Of course!
It's obvious, isn't it?
(audience laughs)
The same applies if we, here, again, replace int by auto,
we have roughly the same rule.
So, I found 19 ways to initialize an int
and that's probably not complete.
Maybe the 19 naive ways to initialize an int.
Well, I think we agree that initializing an int
with parentheses looks a little bit red on this slide.
Too much corner cases, too much tricky things,
we have a problem because function declarations
look like that, so it was an intentional decision
not to use parentheses for uniform initialization.
So, strike this idea.
Which means, by the way, we should maybe
no longer initialize the int i3 with parentheses,
but let's see, this talk is not over yet.
So, let's concentrate on the other things,
on using curly braces.
Let's look a little bit at the auto cases.
This case looks good.
It does what it should, looks good,
and we have the right value, it's intuitive,
I would say; fine.
How about this case?
Well, when we standardized C++11,
we made a significant mistake here.
Yeah, we developed software,
the software is called the C++ standard,
and then the standard, it's like in any other software,
we make mistakes, we have bug fixes,
but only problem we have, like in any other software
or almost any other software,
we have to be backward-compatible,
and that creates a lot of pressure.
But we decided to change the semantic of this.
The initial semantic was that this is initializing
an std::initializer_list of an int.
Well, if you ask a naive programmer,
"Is this what you think that is created here?"
They will probably say no, unless they know
this kind of questions from C++.
Okay, so, we fix this, and by the way,
we fix this officially in the standard in C++14
or for C++14, but we fix it in a way
that it's considered to be a defect,
so at a certain moment when the compilers adopt
these new semantics of this thing,
you get different behavior even in C++11 mode,
which is just that you know that Clang 3.8,
GCC 5, and Visual Studio '15, for example.
So, this is now doing the right thing,
unless you are using an old compiler.
Do we have more?
How about the equal sign here?
Well, we had the interesting decision
that we thought, well, just for generic code,
sometimes it's useful to have syntax
to initialize an std::initializer_list of some thing.
So, a lot of people rejected to make this consistent here,
which means an equal sign in an initialization
can change the type of the thing you initialize.
This is a nightmare.
This is a real nightmare.
I asked this three months ago again on our reflectors,
"Hey, guys, are we ready to change that?"
No, we are not.
I simply asked, "Are we all agreed
"that this is not the right thing?"
No, we don't not all agree;
no, what is double, negation? (audience laughs)
Okay, yeah.
You know what I mean.
So, yes, this will probably remain to be the case
until we fix this, really, we really fix this,
and the intention is probably, and we discussed this
a little bit further, the intention is that
we will not modify the semantics of this,
we will disable this at all.
That's my thought about the likeliness
about how this will become fixed.
Until we get something like this,
so if we have something like the ability
to clear and initialize a list of auto,
so that this can be part of a template,
that the type of the elements gets deduced
from the thing on the right,
and by the way, we might, at the same time,
be able to declare an area of auto here that way.
The roughly idea is that the moment we have that,
we no longer need i11 support.
We have too much to cope with that,
so let's turn it off.
Probably, also, because we can't agree
on what is the usual other reaction.
So, this will probably become an error on C++23,
but you never know.
You never know.
So, I think we might have a consensus already,
but if not, we will probably have consensus
that initializing with auto and the equal sign
and curly braces is a combination you shouldn't use.
Probably not even now.
So, does it mean, well, looking for common pattern,
that we have learned something from this?
You know, I am giving a couple of trainings
to beginners this year, for whatever reason.
They suddenly wanna learn C++ again, or whatsoever.
I have to decide what to teach in these trainings.
I have to decide which one of these 11 or now nine ways
to initialize an int is the right way to start with
for the ordinary beginner, for the naive programmer.
So, maybe the fact that i11 is not working
probably is enough reason to say
"The rule becomes too complicated if you use an equal sign.
"Don't use an equal sign.
"It's better without."
Maybe, maybe.
Because we have other opinions and other things going on,
and there was the first part of initializing with auto,
which looked like this.
Where does this come from?
It came from the idea that we can almost always use auto,
which was an idea Herb Sutter brought up in 2013
in one of his week columns, Guru of the Week blogs.
No, that year we didn't have the term blogs, did we?
No, probably not.
The roughly idea was, after we found out already, there,
that initialization is a mess,
do we have better rules than now, or than five years ago?
The rough idea: let's make it very consistent.
Let's make it whatever you declare something,
whether it's a type or an object,
you start with some general term, like auto for objects
and using for types, then comes a name
and then comes something that initializes the variable.
So, there were some rules in this, I say block.
Usually, just give us an initial value,
and if that's not appropriate, if you need something else,
give the type in front,
so that you see the four examples on the bottom,
that they get converted to the thing on the right.
Is this without any problem?
I hear a couple of hype about that.
Whenever we introduce a new term,
especially if it sounds that well, AAA,
we all know AAA, at least in the States,
it has a special meaning here,
you think, this is a good rule.
Because it sounds simple,
and simplicity is really what we need.
At the same time, you should be careful,
because you know there can't be anything simple in C++.
(audience chuckles)
So, let's look into this.
By the way, I should comment,
Herb explained that to me, that one good thing
of this semantic is you can't forget to initialize
if you have this rule.
Because if you don't have an initial value,
this will not compile.
So, we have the examples I just introduced, we have it,
and now, let's look into some other cases,
some corner cases with this syntax.
The first corner case is using equals and curly braces,
like here on the left.
We had that already.
That was a problem with the solution that we thought
this is an initializer list, so we need the other way,
the bottom line here that is initializing the i.
Another problem is, what happens
with user-defined types like std::string?
And then we have string literals.
How do we write that?
Well, the proposal was, well, let's use the s suffix,
which we have defined via the literal operator since C++14;
unfortunately, that doesn't work in practice.
It's almost never used, because we forget a little detail:
it should always be possible to do this.
Which isn't, you need a declaration
like using namespace std::literals.
I think it's time, now, to make this suffix
globally available, so one of the features where we say
this is no longer a library feature;
in std, it should be globally available.
One argument against that was that we will probably,
sooner or later, get std two, and then we get some conflict,
but this discussion is over.
With the adoption of the concepts in C++17
that was the biggest and most dangerous features
we've talked about, which would be a reason
to introduce something like a new standard namespace,
it turned out in practice when we discussed the details
that we get a chaos, a fundamental chaos,
when, before that, introduce a new namespace.
In fact, we would have to add a new namespace
for each and every new library feature
that comes with something new that breaks existing behavior
to make this work, or we would have to be conflict-free,
but if we are conflict-free
among the different standard namespaces,
we don't need the second namespace.
So, std two is gone, trust me.
Well, trust nobody that talks about C++.
(audience laughs)
Good, so, hopefully, we might fix that,
we haven't done that.
Then, we have the problem that this way of initialization
only works for types that are copyable or movable.
So, this would not work for atomic,
this would not work for lock_guard, and other types
that are neither copy nor movable.
We have nother similar problem here.
Sometimes, using this is expensive,
because we have no cheap move support
although the operation is expensive.
The example is std::array.
Std::array can't be moved
because all the elements are on the stack.
So, all the elements have to be initialized
to be copied from the right side to the left side.
The good news is, this is fixed.
We fixed that in C++17, with C++17.
Because both here, now, the initialization of a with auto
will now compile, and the initialization of r
with this syntax is now a fast initialization,
because it's guaranteed that there's no copy call at all.
Why that?
We introduced a rule that, for any type,
even if no copying and no moving is supported,
the following is now still possible,
which hasn't been before.
If you pass an argument by value,
which is a temporary object, or for the experts, a prvalue,
this is guaranteed not to become a copy or a move.
In the past, the compilers had the freedom
to optimize things the way here,
but now they are required to do so,
and because they are required to do so,
we can guarantee that this works,
even if no copy or move constructor is defined.
Copy and move constructors not defined.
The same is true for return values.
If we return a temporary object, it's no longer necessary
that the data type has to support copying or moving.
His only applies to the unnamed return-value optimization,
not to the named return-value optimization;
there you still need something,
because the return-value optimization uses lvalues
or something similar.
You can then initialize the whole thing
and pass it to an object as an initialization,
and look at this: when we create, or when we, in bar,
have this return statement, then you,
mental model of C++ is that this does not create an object.
This creates an initial value, and this initial value
is parsed as a return type and used to initialize x,
and there, we have for the first time an object
with a location in the program
where you can take the autos from.
So, even if you take the return value
and pass it to foo because you pass it to foo,
you still haven't create an object
until you get to the moment where, inside foo,
the parameter is declared.
When you have something like I wanna use the return value
by reference, this is an interesting case,
because for the moment, until bar returns something,
we have no created object here, but the moment we need,
here, a location for this initial value,
this object, this return value, becomes materialized.
By the way, I should just use the new syntax, of course,
here, and not the old syntax here,
but that's something we discuss right now.
So, what happens here is we return a temporary object,
and because we need its location,
because, for example, we need a reference or a pointer,
we materialize the object.
This is a new mental model
for the value categories in C++17.
The mental model has not changed the terminology,
so we still have lvalue, xvalue, and prvalue
of the lower level, each expression is one of this;
glvalue and rvalue as common terms.
We have slightly changed when a prvalue gets converted
to an xvalue; in fact, the moment a temporary object,
a prvalue, needs a location, it is what we call
temporary materialization conversion happens,
which is an implicit prvalue-to-xvalue conversion.
I hope I understood it correctly; I am only using C++
and I asked the experts about 20 times about this.
Good, so, that being said, we are good now
with the case of atomic and std::array.
So, let's go to some other problems.
If we have a type name consisting out of multiple words,
this is still not supported, so this will not compile.
So, there, maybe we have a special case.
Maybe we have another type which might
or might not look the same, or we have to use
a static_cast here, which gets a little bit annoying,
and then we have, finally, the problem
that if we use references, or if you want
to use a returned value by a reference,
this does not work if on the left side is auto.
Why that?
Because auto decays.
Auto follows the rules of passing by value;
these rules were adopted from C, and that means
when you have an integer declared and you have a reference
to this integer, and you initialize an object
declared with this reference, this type of r
is not the type of v.
The decayed type of r is the type of v,
so that means v is an int, and not an int reference
or a constant reference, so this is the new object.
Because of this rule, for example,
when you initialize with auto and you take an array,
this becomes a pointer, and that means that the s here,
if you initialize by a string literal,
which is the type is an array of constant characters,
this is also a point becoming a pointer.
So, this is different here, so we create a new object
instead of referring to the return value.
The solution has to be, then, to use auto&&
depending on the semantics you wanna have.
If you didn't understand them, yeah, they are tricky.
I don't explain them here.
The interesting question is, which I thought
when I provided these slides,
maybe I should always use auto&& here?
I didn't think it through,
maybe somebody of you can do that,
but it would mean that the AAA rule changes.
First of all, the Almost, the first word
of Almost Always Auto, came because of the atomic
and array case.
So, the first A is gone, it's now only Always Auto.
But now, we might use additional references,
so I proposing to rename this (audience chuckles)
to the quadruple-A rule.
So, to say, this is Almost Auto, and then reference.
Well, A? Is A a reference?
No, that's something different.
I don't know. - Ampersand.
- Ampersand, yeah, that was the real one, yeah.
Sometimes I forgot my own ideas, yeah.
Always Auto Ampersand Ampersand.
That's easy.
(audience chuckles)
You only get famous in this community
if you have crazy ideas.
(audience laughs)
And you know what, next year, everybody will say,
"Oh, there's a new simple rule Nicolai Josuttis taught us,
"we should use them everywhere."
(audience chuckles)
I see that coming.
Don't follow guidelines from experts who have crazy ideas.
So, what we learned is we initialized i12,
the AAA or the AAAA rule or whatsoever.
This does not always work; most of the time, but not always.
Now, I am here, and wondering myself, what should I teach?
What should I teach to beginners?
Let's talk a little bit about the benefits
of uniform initialization.
First of all, initializing with curly braces
sometimes help and makes things less complicated.
The first example we had, when we have using a parentheses,
this was considered to be a function,
not to be a declaration of an object,
and in template code, when we had T parentheses,
we had to need an equal sign.
So, this now becomes simpler; in template code,
we can write T x3 and then use empty curly braces there,
and we guarantee that the object is initialized.
Here's another example I like.
If you call a hash function, you need two things.
You first have to create the hash function,
and then, you have to call it.
Because it's a function object,
and the way you would do it the old way
would say you calling twice with the parentheses here,
the first parentheses are in fact for being a declaration
and the second parentheses are for being the function call,
which I would now be easier to distinguish
by having different syntax here.
Another thing that turned out that I learned,
I don't know, three months ago,
when I, on the floor, discussed with some other guys
were adopting reflection API of C++ for C++20 or 23,
they came up with this problem, and they wanted to reflect
on an expression called string().
The interesting thing was, they were discussing
whether they give and how they give string()
different semantics so that they can express
whether they reflect on the type,
so I call the default constructor here,
or whether they reflect on the constructor call,
on the function.
It was funny, because I was remembering my old days
when I thought, "Hey, why should we do that?
"We had this problem before,
"so why do you need something else?"
If you want to reflect on the constructor call,
use curly braces.
If we want to reflect on the type, use parentheses,
because that was the solution I learned
20 years ago for a similar problem.
20 years ago, I wrote code like this.
When I wrote the first edition of the C++ standard library,
I ran into this problem.
I was using namespace, which is important here,
I was using namespace std, and then, I was declaring,
I thought I was declaring, an object v of type vector
initialized by two objects that read from the input stream,
so, from cin, and that's the way you can do that.
It turned out that this compiled.
But the moment I tried to use the vector,
I got the incredible bad error message.
Because, what was happening here,
I didn't declare an object, I declared v to be a function
with a return type vector and two parameters;
the first parameter had the name cin
and was of type istream_iterator,
and the second parameter had no name,
but it was a parameter for a function taking no arguments
and returning an istream_iterator.
That's obvious, isn't it?
(audience laughs)
We call this a most vexing parsing problem, or one of them,
which means when we pass, we try to pass it
as good as possible, and in doubt,
we interpret it that way.
I'm not a core expert, I needed a couple of core experts
to explain me what's going on here, so thanks to John Spicer
for this explanation 20 years ago.
The solution was that we needed
an additional pair of parentheses.
You know what?
If we would have had uniform initialization,
the solution is easy.
Uniform initialization says use braces,
braces can't be interpreted as a function call,
so the problem is gone away.
So, what else do we have?
We have another benefit, which you probably all know:
the curly braces disable implicit narrowing.
So, when we, coming from C, initial an int with 7.8,
this works, this doesn't even give a warning,
and you get the value of seven; then you initialize
an int with curly braces, this is detected.
When you initialize an unsigned value
with a maybe-signed value, this is okay
if you use the old way to initialize,
whether it's the assignment operator or the parentheses.
Using uniform initialization, this is detected,
and this also applies to vectors of int taking doubles,
whether as a literal or whether as a type.
Not all compilers follow this rule,
so in GCC and Clang, please enable --pedantic-errors
to get this feedback.
Because narrowing is always bad, isn't it?
(whines) It's not that way.
Look at this simple example.
You have the character c, initialized with a,
and I wanna increment this character.
What I might do is I initialize another character
with c plus one, which is narrowing,
because, by definition, the sum of a character and an int
is an int, and using an int to a character
might lose its value.
So, this will not compile, and in this case,
it might be bad.
People would have to write a static_cast
or they use the other way of initialization we still have.
Thank goodness.
So, the good thing what you learn from that
is not the case that you should say
always use brace initialization.
Use it maybe as a default, but for special cases,
use it the other way 'round.
There are other examples like that.
Here's another example that is pretty well known.
We initialize objects,
we can initialize them the old-fashioned way
and the initializer-list way.
By the way, we have some interesting terminology
confusing here, because what you have there on the bottom
is four initializer lists, but what people often mean
by initializer lists, they mean standard initializer lists,
so standard initializer lists is a special way
to deal with initializer lists,
and that way, they have to be homogenous.
So, the first three constructors
call the first two constructors, the classical constructor;
no, the first two, excuse me,
and the last two call the last initializer-list constructor.
wait a minute.
Please beware, whenever you need an std::initializer_list,
there have to be curly braces, otherwise it doesn't compile.
That means that default constructor is not supported here,
because there is no classical constructor
taking no arguments.
So, now things became complicated when we did overload
and had different matches.
When we say I have an int constructor
and an initializer-list constructor
and the int constructor has a default value
so it can be used as a default constructor,
there are rules for that.
The rules are pretty simple: without curly braces,
the std::initializer_list can't be used,
so the first three cases call the first constructor
if they compile.
With curly braces, well, it's a little bit more complicated.
If the initializer list is empty,
we call the classical default constructor if it exists,
and otherwise, we call the other.
Easy to remember.
Shouldn't make a difference, should it?
Well, sometimes it does.
Here's an example.
In the standard library, we have a vector declared,
and you could do that in the past,
and if you were not using curly braces,
so that means you didn't use
the initializer-list constructor,
we were creating a vector of three elements of value 42,
and now, with curly braces, we have different behavior.
For some people, this is the proof
that we made something wrong.
For me, it's a proof...
Well, what we made wrong is to introduce initializer lists.
This is a proof, but this is not a proof
that initializer lists is wrong,
this is a proof that the constructor of a vector
that applies to the first rule is wrong,
and we should not standardize it any more,
because any naive programmer would say,
"Yeah, v2 is the thing that should happen
"in all these cases," if it compiles.
So, this is, again, a good example that we might sometimes,
because we need the first behavior,
use a different way of initialization,
but the curly braces are self-explanatory
and therefore better to teach.
So, how can we initialize the vector of string?
Well, let's look at a vector of string
in various cases.
The first string doesn't compile,
because this uses the initializer-list constructor;
remember, the elements are string,
so it has to be convertible to a string,
so we have no curly braces, this will not compile.
About the second case, we have to talk
a little bit in a moment.
Then, we have multiple elements assigned,
and then we deal with additional curly braces, just for fun.
Did you ever do that?
It turned out that v04 or v05 are okay, v06 is not,
and v07 is interesting.
Because it's a fatal runtime error.
The problem is this has not the right number of braces.
Either you have to have the right number of braces, roughly,
or only one pair of braces; that's not the exact rule,
I didn't understand it, ask the core guy.
The problem is, what happens here is
we interpret these two parameters as iterators.
So, these are iterators; that means that is the begin
of the range, and that's the end of the range,
and they should refer to the same range of characters,
because characters convert implicitly to strings,
this will compile.
If you are lucky, you get a core dump.
If not, you have a big problem.
So, yeah, things like that happen.
That's, by the way, not the fault of the equal sign.
You have he same problem without the equal sign.
So, not everything works fine in both ways.
Let's look at enumerations.
Enumerations, we initially introduced something in C++17,
which was a rule saying when you have an enumeration
with a fixed underlying type,
then, it's okay to initialize an enum
with using direct-list initialization,
which means using curly braces and no equal sign.
This is the only developed thing.
Anything else will still not work.
So, we made s3 compile in C++17
in case we either have an enum class or we have an enum
with this explicitly-specified underlying type.
If you have a classical enum
without a specified underlying type,
this was still not compiling,
you have no way to do that without a static_cast.
The reason we introduced that was, for example,
to support something like std::byte,
that we can declare an object like std::byte easily
by having at least one signature to give it
an initial integral value.
Next question: direct or copy?
We have explicit.
Explicit plays an impact when we initialize.
It only plays an impact when we initialize
with the equal sign.
Without, it doesn't matter, but with, it does matter.
So, look at these initializations here for Collection.
We say we don't want to have an implicit conversion
from into to Collection, so let's look at this.
Without the equal sign, everything compiles,
doesn't matter whether we have explicit there or not,
but with, we have to double-check
is there an implicit conversion, and there is.
Because the thing on the right has a different type,
so the constructor has to called,
and we don't explicitly specify on the right side
to which type we wanna explicitly convert.
So, this will not compile.
Things become interesting if you have something like this:
you have a collection, and you have sometimes explicit
and sometimes not.
So, you get the following behavior
that, for example, due to the rules we had,
initializing with curly braces might sometimes work
and might sometimes not,
because without a value in sight,
according to the rules we had,
the default constructor is called, which is not explicit.
Otherwise, the other constructor is called,
which is explicit, and that also applies
to initializations with equal sign, so, copy initialization.
Which, of course, confuses a lot of people,
and we made every mistake also by the standardization,
so here's an example which we had,
which is class vector and C++11; look at this.
Class vector has default constructor being explicit
and initializer list not being explicit.
This should never happen.
Make sure the default constructor
and the initializer-list constructor
are either both explicit or none of them is explicit;
this should be a very, very important style guide you have.
We fixed that, because, look at this,
it was totally confusing for people that v3 didn't compile.
We fixed that in C++14.
So, we now know that here in the case v01,
this will compile since C++14.
How else do we deal with explicit in the vector?
Well, look at this.
We have here class Person.
If you have both of your arguments or no arguments,
now explicit also matters.
That means a person can be implicitly initialized
by an empty curly braces, or a curly braces
with some values inside.
So, we have to think about whether we like that or not.
We can enable this all, we can disable this all,
then the programmer has to write, everywhere, Person,
or we can partially enable things, like this,
because the argument here is it's always critical
if you not intentionally do something else
than just writing a name,
so it should be that you have to do something else,
and if it's only empty curly braces.
It turned out that's the direction (blathers).
We have fixed a couple of cases
or about to fix couple of cases for that for C++20,
so you have the following interesting behavior;
look at this and look at the bottom group here.
We can initialize with the empty curly braces
and equal sign, we can with a single value,
and from time to time, we can even with another value,
because we make the mistake
that we declare both constructors together,
because the second is a default value for the first,
and then with multiple arguments, it's again covered.
We fixed that; probably, there is currently ongoing
a task to fix, for example, v11 here
so that this would compile in C++20.
I don't know whether this is improvement or not,
because, still, we have some cases
where you need the explicit name and sometimes not,
but that's the way it is.
Okay, having said that, let's talk about aggregates,
which are structs or arrays traditionally from C,
and the meaning of structs or arrays changed over time,
changed over time due to different reasons,
we have now the new definition of it in C++17;
for the first time, we support base classes.
So, here's an example of what is supported right now.
Your aggregate, Dv is the aggregate here,
can have base-type data, and this was before class,
so there was this strange rule that...
No, that was not the strange rule, that was okay,
so that you could declare, initialize that,
or you could have that uninitialized.
Then, C++17, this is an aggregate,
so you can now do things like this: you can pass values
to the base class in the initialization.
Something, when I thought about it, I learned,
was the following thing: if you have a deleted constructor,
which means you have a user-declared constructor,
this was still an aggregate.
This was introduced in C++11 half-intentionally,
I would say.
Immediately, people thought, "Oh, this is cool.
"By this trick, I can force the programmer
"that they have to use initializations with curly braces."
But it was totally unintentional.
I asked everybody, "What does it mean?"
"Oh, it has to delete the default constructor,"
so c2 should not compile.
We fixed that now, and we fixed that
couple of months ago in the last meetings,
and this will be fixed in C++20,
having the old meaning again.
We still have some other problems.
Here's another problem: if we have explicit here,
and this is an aggregate, then you can still
have this problem, that you say this aggregate,
default initialized, but initializing with curly braces
doesn't work, and even worse,
initializing using the parentheses initializer, as you see,
the curly initializer is the difference.
So, with curly braces, this does not initialize,
and that's the reason why template code
in the standard library still uses parentheses
to initial local objects, and not using curly braces here.
I hope we fix that soon.
So, what else do we have?
We have an array type, which is an aggregate.
It's just a templified aggregate in C++,
so we took a C structure,
we took the C structure and templified the arguments,
added a few functions so that you can use it
like a container, but it is not...
It is still an aggregate,
so it follows different rules for initialization.
One consequence is if you declare an array
without curly braces, it will be not initialized,
the values will be not initialized.
But there's a more interesting consequence.
Think about you switch from a vector to an array.
We have a vector of complex doubles.
How do you initialize that?
For example, that way.
If you switch to arrays, this will no longer compile,
because the way you have to do that
is to use an additional pair of parentheses,
because this is, in fact, a struct
with an array element inside, and the array elements
are directly initialized.
Okay, doesn't compile, it's not that bad; look at this.
Let's initialize a vector of complex numbers
with one element, and the element has a value (1,2).
Let's take this code and replace vector by array:
you have different semantics.
Because now, you have a level higher than something,
which means you have not initialized the first element,
you have initialized the first two elements,
the first taking the one and the second taking the two.
So, what you have done is in fact this.
Also some things that drive me crazy.
Do we have other issues?
Atomic was, according to the standard,
not initializing even if you use curly braces.
I had to teach to you, whenever you use curly braces,
the objects are initialized unless you use an atomic object.
Crazy, and the reasons were even crazier,
because this was the trial of future compatibility
to a hopeful consistency with C
in the hope they adopt some feature similar to this,
which they didn't.
So, we broke our whole rules because of that,
and by the way, it, even then,
should not have happened there.
So, this will be fixed, thankfully, in C++20,
the compilers don't follow the rules there, formally,
even though they're allowed to initialize.
It's not undefined, yeah, but they are not allowed
to initialize here, but they do. (chuckles)
So, we fixed that in C++20.
The final thing I want to talk about is assert.
We establish these rules in company,
so using brace initialization, uniform initialization,
and sometimes, we had assert and we found out,
oh, wait a minute, if I switch from comparing my C
with the complex(0,0), and I replace complex(0,0)
with curly braces, this will no longer compile,
because for the assertion, which is a macro,
this is becoming something that has two arguments.
The first argument ends with the first zero,
the second argument starts with the second zero.
So, please, then, you have to take
another pair of parentheses.
Okay, we fixed uniform initialization,
we fixed the mess of initialization,
we made it uniform, but we had to be backward-compatible,
which, by the way, increases the mess,
actually, because we still have it.
So, how do we fix it?
Of course, we have to provide a style guide,
and you have seen a little bit
of the style guide I would propose.
I was discussing this: if there's a couple of companies,
and if I discuss it with them,
we always stuck at the following place
when I said, "Look at this: you initialize that way,
"and you initialize a for loop that way;
"why don't you change your style and make a consistent use
"of only using curly braces?"
Which means change the style
you write your traditional for loop.
That created interesting reactions. (chuckles)
(groans) I fear, not that very well. (hums)
You know what?
In the company we had done that,
which is a company where I now have given
five to 10 trainings for 200 people,
we got used to it, too, like this.
I feeling now uncomfortable using the equal sign
for initialization, even in this case.
So, sometimes, give it a try.
It might not be approved, but we have
very well experience with that, to say,
well, let's everywhere use curly braces now
without an equal sign.
You have one very simple rule, and not dogmatic.
If it doesn't work, there are other ways to initialize,
and that's it.
So, I have provided a style guide.
This covers an area where Herb Sutter did some research
about how to consolidate style guides,
and he said 5% of all style-guide stuff
is about initialization, so this is an important topic.
Unfortunately, they don't all say the same.
(audience chuckles)
That's our problem, though, let's look at Abseil.
Uniform is a stretch, that's one reason
you shouldn't use it, and it's not exactly intuitive.
I would claim the opposite, but okay.
Interesting, I found this sentence:
how much should we change our habits?
"We don't like it."
(audience chuckles)
And we don't believe the benefits outweigh the drawbacks.
Okay, everybody has its own opinion.
I think they're wrong.
By the way, what they recommend instead is this:
use assignment syntax when initializing directly,
and they really list all the types:
literal types, smart pointers, containers,
and when performing struct initialization
or doing copy construction.
Otherwise, use the traditional constructor syntax
with parentheses, and if that does not work,
use curly initialization without the equal sign;
though it's the third place, it's my recommendation.
And never mix curly braces and auto,
because then you run into this problem
with your first three rules.
I have a different recommendation, which, by the way,
fits roughly with the Cpp core guidelines:
prefer the curly braces.
I think it's time to make this common sense
in this community, and to stop all the discussion
about this is worse.
This is far better now, this is really far better now
to teach, and to help people not to make many mistakes.
This is not the only way for initialization,
I'm talking about the default way to initialize.
So, make your initialization uniform, for the first step.
We didn't succeed in C++11, you have seen a lot of fixes
that came later, in C++14, 17,
and some of them will come in C++20.
So, that's one example that,
in this community, as we are community-driven,
it's cool to provide a new feature;
it's not that cool to provide the hard work
of making it completely right
so that people don't fall into traps.
And it's not their fault,
they are not getting paid for that.
Somebody has to do the work,
just because they have nothing better to do.
So, a couple of the proposals you have seen here,
I propose because I am fed up
teaching this current situation to beginners.
The next thing is provide style guides;
we have different ones.
The next thing is consolidate the style guides.
Consolidate the style guides, and that's the thing
we have to do right now.
It will be a big task.
Uniform initialization is only one example,
I have a couple of other examples
where the first day of C++, I have no idea what to teach,
and that's a big problem we should fix.
I have no time for questions, but I am around here.
I hope you will bring this message to the community.
It's really my intention that we can solve this forever.
Thank you very much, and have a nice week.
(audience applauds)