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

Video thumbnail
- All right, folks, thanks for being here.
My name is Louis Dionne, I work at Apple.
I work on the standard library there,
so lib C++, which is the standard library ship with Cling.
And in a past life, I was a crazy template meta-programmer.
(audience laughing)
And as part of my duties on the C++ standards committee,
I'm quite interested in reflection,
compile-time programming,
code generation, meta-classes, these kind of topics,
also like lambdas.
And so the goal of this talk here is really to
kind of clear up some misconceptions
or rather some confusion about our plans
for compile-time programming.
So, I wanna give you a bird's eye view of what's
what we expect is going to happen in the future
for compile-time programming in reflection.
That's kind of the goal of the talk.
Hopefully if I succeed, you'll leave this room less confused
than you are right now, with respect to
what we're gonna do in C++20 and beyond.
So, before we start getting into the nitty gritty details,
I think it's important to ask ourselves
why do we even care about that,
'cause C++ is already a complex language,
and if we're gonna solve problems, we
if we're gonna increase the complexity of the language
by adding stuff to it we need to have
some good reasons to do that.
For compile-time programming, one of the biggest
used cases is constant initialization,
where we have basically data that we know compiled.
I'm gonna have information that we know at compile time.
And we wanna perform some kind of computation on that data,
and have that computation perform
exclusively at compile time, nothing at run time.
And then somehow have the compiler store that information
for us somewhere in the program,
such that we can then use it at run time.
So for example, here I'm generating
exponentials from zero to a thousand,
and I'm storing that in an array
that I can then access at run time.
If compute_exp is
is constexpr, then in principle there is
absolutely nothing happening at run time
to initialize that array.
So there is no static initialization,
there is nothing happening when the program starts up.
It's just hard-coded in the data segment,
which is what we want.
So that's one used case for compile-time programming.
Another thing is boilerplate reduction.
There are many cases in C++ where even to do simple things,
we need to tediously repeat
patterns like that.
I mean, I'm pretty sure everybody has one of these
in their codebases.
And sometimes, they're generated using macros.
And sometimes, they're just generated by hand,
or sometimes you even have a Python script
or something like that that generates them.
We've all seen that.
And actually Cling is full of that kind of stuff,
where we generate AST nodes and stuff like that,
then it's very, very, very annoying to
not to be able to rely
on the compiler itself, which has this information,
because the compiler knows the enumerators.
It's really annoying the fact that we can't rely
on that knowledge.
Solving these topics, these problems,
you used to be super nerdy,
like given amongst a C++ crowd, which is
which is quite something to say.
But it turns out that recently
recently everybody is on it.
Everybody is producing papers, talking about that.
I mean, the direction group even
the direction group even included reflection
as one of the major work items for the future revisions
of the language, which is quite something.
I mean, I never expected that this would happen.
But it is a very hot topic right now.
And so as a result, it's super exciting.
We see a lot of different proposals,
but it's also very confusing,
because not all proposals are
I mean, it's really hard to see how proposals fit together,
and sometimes they don't, and some ideas are good,
some ideas are not so good, and it's very difficult.
It's not reasonable to expect that someone with a day job is
actually gonna keep on top of all that.
So what I'm gonna try to do today is put
some order in there and try to give you a little bit
of a bird's eye view of what's coming
and how it fits together essentially,
'cause my job is somewhat to do that summary for you guys.
And so what's the plan?
Even though this is C++, we do have a plan.
(audience laughing)
we're gonna try to make almost everything constexpr.
So, we want to expand constexpr such that it can
basically anything that you can write in normal C++
you can write in constexpr and run it at compile time,
not everything but we wanna expand the language
well, constexpr as much as we can to close that gap.
So that's the first step.
Then we wanna add an API,
that allows you to speak to the compiler,
to query things from the compiler.
The compiler has a representation of your program.
It parses it, does analysis and so on.
And so it knows everything about your program.
What we want is simply a way to programmatically query
the compiler for the information that it already has.
So the first one is just constexpr.
The second one is really
what we call reflection.
It's the ability to reflect on your program.
And then as the last step is the ability to
actually influence the AST,
influence your program programmatically,
meaning you extract some information,
you ask the compiler give me the list of numbers
of that type, for example, then using constexpr you go and
maybe modify these numbers or create new declarations
or whatnot, and then you wanna be able
to go back to the compiler and say, hey,
I want to re-inject that basically
into my program programmatically, that's kind of the idea.
And so we've got features to enable one,
we've got features to enable two,
and we've got features to enable three.
And these three things are pretty much disjointed.
They kind of build on top of each other,
but in some way they are orthogonal,
so there's not one feature that sells all of this.
And it's really important to keep that in mind.
So the first step is expanding constexpr.
And so doing that is necessary if we wanna have
a computationally complete mechanism,
with complex data structures at compile time,
which is what we want, 'cause we wanna
run non-trivial algorithms.
We wanna be able to manipulate data and store it in
not only in a std::array.
We really wanna be able to do that and
maybe have a vector or maybe have a map at compile time.
So we need to expand constexpr.
And so in C++11, constexpr was really, really limited.
Basically, we only had a single return statement,
no mutation, no control flow really,
so we basically turned into functional programmers
for better or worse, and
that's what it was.
In C++14, we unblocked many, many used cases
by allowing control flow, for loops, mutation,
and things like that, inside constexpr.
But we're still limited.
There's still many things we can't do.
And I don't have a lot of love for reinterpret_cast.
That is still something we can't do there.
And so we can't allocate, we can't have try-catch,
we can't throw exceptions,
we can't do virtual calls,
and there is a bunch of stuff like that that we can't do.
And it's a problem, especially allocations,
because it means we can't have variable-size containers.
We're stuck with std::array
or stack-allocated things basically.
So if I wanna implement a keep_if algorithm, for example.
It's basically standard copy if,
which basically takes a range of
integers and then keeps
everything that satisfies the predicate, pretty simple.
So, I might be tempted to just store the results
in a vector.
But I can't do that if my algorithm needs to be constexpr,
because vector doesn't work at compile time, basically.
It's not the literal type so the compiler's gonna tell me
that can't call this function as a constant expression.
So we have to use a fixed-size array, that's really painful.
It is painful because it cannot be resized,
which means that for anything where you don't know
the final size of you container, you're in a bad place.
So what I have to do here,
I have to have my keep_if algorithm
receive the end-size of the array that I'm gonna produce,
which means that before I even execute my algorithm,
I have to know part of the answer.
I have to know how many things am I gonna keep.
And so here I made the choice to actually just pass it
manually, so I basically ran the algorithm in my head.
I was like, all right, I'm gonna keep
three things in my array.
So I passed it on line 16 here.
I passed it to the function,
but that does not compose at all.
So it's really, really hard to actually build
complex, compile-time applications using constexpr today,
because of this inability to actually resize containers.
And so that's why we see, for example, in Ben Deane's talk
where he did the compile-time parsing.
I mean, it was really, really, really difficult,
because you don't know the size of everything
beforehand, so you need to basically duplicate
all your code and go make sure that you have enough
that you allocate enough size in advance.
And so one thing that we're working on is actually making
bringing allocation to compile time.
So we wanna allow new expressions inside constexpr,
and we also need to make standard allocator work
at compile time, because that is
what the standard containers use.
And another thing that this paper does is
what we call the promotion to static storage,
which I'll talk about later.
So the idea basically is to make the following valid.
So if you look on line three, here, I'm allocating an array.
And I don't recommend doing that.
You should be using std::vector,
but I'm just trying to illustrate, so
so the idea here is that you can
actually allocate at compile time, and on line 13 you can
also de-allocate at compile time.
So what the compiler does there is pretty simple, actually.
You have to remember that when
you're executing a constexpr function,
you're really running inside the compiler.
It's an interpreter for the C++ language,
actually for a subset of the C++ language.
And so what happens there is you request memory
and the compiler is not gonna an array of ints, of course.
It's gonna allocate a data structure inside the compiler,
interpreter, that represents an array of ints,
that's what it's gonna give back to you.
So what we're saying basically is we're just expanding
the subset of the language that the interpreter
that exists inside the compiler,
the subset that it supports.
We're just expanding that subset.
And so once we've got that and this is
this is gonna be in C++20, unless things go really wrong.
This is making progress inside the committee.
And so once we've got that, obviously
we should be making standard vector constexpr.
So let's go and try to do that.
Problems is it doesn't work because vector uses try-catch
for exception
to give the exception guarantees in push_back
and other functions like that.
And so if you try to actually just make vector constexpr,
even with the rest of the paper,
you realize that it doesn't work.
And so it's turtles all the way down.
You go and you allow try-catch blocks inside constexpr.
And so this is what we're doing.
We're also allowing, enabling, try-catch blocks
inside constexpr functions.
The idea here is that currently
even if you don't throw, if you have a try-catch block,
it's not gonna be a constexpr function.
If syntactically there is try-catch block,
it can't be a constexpr function,
so this kind of restriction is quite bad, actually,
because it prevents us from
well, in this case, it prevents us
from making vector constexpr, but
but the idea is that
the idea is that
we're not actually making throw statements valid
inside constexpr, here.
So, it's not quite as cool as you think.
You can't throw, so we're just making try-catch blocks valid
but not throw statements.
And so let me explain what happens here.
What happens here is on line 14
well, okay, so what here is I'm just querying a sub-range
of an array, and I'm using
An can throw, if I reach out of range.
So that's why I'm using a try-catch block here.
And on line 14, I'm not reaching outside of the range,
outside of the bounds, so it's all good.
On line 15 though, I'm accessing the array outside
of the bounds, so I'm throwing.
And this is where it fails.
It doesn't fail in the try-catch,
it actually fails when I throw inside array at,
because I haven't allowed throw expressions
inside constexpr.
And the reason why we're not doing that right now is because
it would require the constexpr evaluator
to actually support exception handling properly,
which includes stack unwinding and other things like that.
And doing that at compile time is crazy.
I'm way, way, way too scared of being stabbed in my sleep
by a compiler implementer to actually propose that.
So instead what we're doing is we're just saying,
okay, you can have a try-catch block,
but you can never throw.
Throw, right?
So if you throw, the expression as a whole is gonna be
not a constant expression, which means that if you try to
initialize a constexpr variable from that,
you get a compile-time error, which is what you get today.
So in the future, we can absolutely extend that.
I don't think I'm gonna be the one writing that paper,
because like I said, I'm a little scared.
But it is technically possible to allow throwing exceptions
and catching them, and doing all that kind of stuff.
But for now what we're doing is we're just
syntactically allowing try-catch blocks,
but there are basically no ops,
because you can never throw.
Does that make sense for everyone?
So that way we can
this is like the shortest path
towards making vector constexpr, basically.
And so if we go back to standard vector,
in C++20 this should just work.
So you can just have a standard vector inside constexpr,
so you can push_back, everything's nice.
If you get
if push_back throws for some reason,
even though there's a try-catch somewhere in there,
or even if you try to actually catch it,
like catch bad alloc at compile time, it's not gonna work.
You're gonna get a hard error, basically.
The compiler is gonna say, you can't throw.
Make sense?
And so one thing that I mentioned
but didn't explain is promotion to static storage,
which we also call non-transient allocation, for standard
for people that like the core working group.
So, promotion to static storage essentially is
answers the question of what happens
when a constexpr object is used at run time.
What happens when it leaks to the run-time contexts.
So imagine this.
Imagine this here, where I have a constexpr vector
with a lookup table, which is a lookup table,
and everything is filled up at compile time.
But then I use it at run time.
I use it by accessing
accessing the table at indices that are only known
at run time, so I don't in advance which parts
of my lookup table I'm gonna be accessing.
And because I don't know that in advance,
it means that the compiler somehow has
to store the full table in the program.
It has to be all there, because I could be accessing
any parts of it at run time.
So we need to make that possible.
So how does that work?
Well, it's actually pretty simple.
There are a couple of rules here.
So if a constexpr allocation leaks to the run time,
if you use it basically beyond constexpr.
And if the destructor would clean up the allocation,
if it were called, because here the vector
it's a constexpr vector, the destructor is
never really called.
So if running the destructor would
actually clean up the allocation,
then we promote it to static storage.
That's kind of the idea, here.
So that's actually nothing new if you think about this,
because if I turn this into a std::array
instead of a std::vector,
I'm already putting that in the data segment.
I'm already promoting to static storage, today.
So this is nothing new.
The main difference is that here
you have a contiguous chunk of the data segment,
which represents your array.
Whereas in the standard vector case,
you have a little place in your data segment,
which is the three pointers,
which point somewhere else in your data segment
to a contiguous block of memory that represents the vector.
So basically we're making it possible to serialize
a little more complex data structures to the data segment,
that's kind of a way to see that.
Can you go to the mic, please.
- [Audience Member] If you have the vector
and during run time, you expand that vector
what-- - And you what at run time?
- [Audience Member] Expand the vector.
- All right, so you can't expand the vector at run time,
because it's a constexpr vector.
So you cannot call any non-const methods on it.
And actually I'm gonna delay questions to the end,
just because I wanna make sure I don't run out of time.
But you can note the slide number.
So it's a constexpr variable, which means that you can't
actually modify, you cannot call
any non-const methods on it.
So you compute the data structure, and then you burn it
into the data segment, and it doesn't change anymore.
But you are free to look up in the data structure,
you're free to use it as you wish.
And so one interesting thing here
in the way we're wording these changes is that
if executing the destructor would
not clean up the allocations that are leaking to run time,
then it is not a constant expression.
So you get a compiler error, because you're trying
to basically assign something that is
not a constant expression to a constexpr variable.
Take a minute to think about that.
This means
that has a corollary,
if you can call a function at compile time,
it means that it is leak-free for the set of inputs
that you provide it.
This is pretty cool.
We're putting Valogran out of business.
(audience laughing)
So, you actually have a leak checker inside your compiler,
that way, which is really cool.
I would not rely on that only to check your leaks,
because you're not actually using systems_malloc,
and you're running basically inside an interpreter,
which is not quite the real thing.
But it's still pretty impressive.
And actually if you have a friend or coworker
that's a compiler engineer, buy them drink,
because this like
this is huge.
I mean, the complexity of C++ compilers now is
such that we have a leak detector at compile time,
that's pretty cool.
And so we've got other things that we
in the pipeline for constexpr,
like basically library-level additions.
And so one of them is standard string,
which is quite obvious.
Once you can allocate, why not have standard string work
at compile time?
So there are some challenges to that,
which I can explain a little bit later,
but this is in the pipeline.
The paper is like half-written,
so I'm gonna try to get this one for San Diego
in a couple of weeks.
And so standard string should work at compile time.
There's no reason not to.
And then standard map, unordered_map.
Set and an unordered set, why not?
Why stop anywhere?
Where do we have to stop, right?
All these data structures, I don't see
any reason why they wouldn't be constexpr-friendly, really,
with the new allocation changes.
Optional variant.
Everybody likes them, expected.
All of these helper classes should
it should be possible to make most of them constexpr.
Some of them might use dirty tricks,
like reinterpret cast and things like that, internally.
So, we're gonna have to be really careful about that.
So, we still need to survey some of them,
but at least for standard string and standard map and set,
maybe not the unordered variants but
for map and set, I'm pretty sure we should be able
to have them constexpr.
Maybe by '20 or '23, depending.
And the math functions, I know there is a lot
of people that want that, because then
you could precompute tables that include
that use the math functions, so that would be quite useful.
Another thing is standard thread.
I'm just kidding.
(audience laughing)
That's the way to solve the compile time problem,
just compile multi-threaded, everything's fine.
So the little smiley there is me
after getting punched in the face
by a compiler implementer.
(audience laughing)
And so honestly there's almost no limit
to what we can do there, I think.
It's just about surveying
the standard library implementation.
So you go there, you look at it,
and you're like, all right, what do I need
in the language in order to just throw
like to span constexpr everywhere.
And usually there's not a lot missing.
With the few papers that we've added,
usually there's not gonna be a lot of stuff missing.
So if you have your personal preference
for having something constexpr, just go and produce a paper,
I think that that
if you survey the implementation and your confident
that we can make it happen,
and then you can defend your case,
I don't see any reason not to do it, really.
However, there are some challenges.
So, if you do survey one of the implementations,
which is what I did for vector and string,
you run into a few problems like reinterpret_cast
or any kind of dirty trick like that.
For example, we used some dirty tricks when implementing
the small string optimization for std::string,
so we have to work around these things.
Sometimes, we use built-ins that are not constexpr-friendly
as well, so we need to work on those.
And there are some things we can't do,
like raw memory allocation.
There's just no way we can do that.
So, allocating with new is fine,
because you tell the compiler which type you're allocating.
Allocating malloc is not, because you're not telling
the compiler which type you're allocating.
And actually, I wanna pause on that, and really
something needs to sink in here.
At compile time, we need to catch
all the undefined behavior, it's super important.
If the compiler can't catch all the undefined behavior
at compile time, it means that it can run.
If we can induce a compiler into UB,
it means that the result of running the compiler,
your compilation, is whatever.
So, you could crash the compiler,
but you could also produce an invalid program,
or you could produce a program that is valid
but doesn't do what you want.
So at that point, it's super scary.
So, the compiler has to catch all the UB at compile time.
It has to track everything,
which is why we have some limitations, so
like reinterpret_cast and raw memory allocation, like that.
There is almost no way we can do that,
because the compiler would have to track
every single thing you do with a chunk of memory.
It just ended you to make sure
that you're never shooting yourself in the foot,
which is very, very difficult.
And that's something that scares implementers a lot.
So raw memory allocation, we probably will
it's not clear that we're gonna do it ever.
And there is other annotations if you look
at your C++ standard library.
There's other added annotations like UBSan
or ASAN or things like that
that are used, and we
they're inherently not constexpr-friendly,
so we need to work around as well.
And so this is where actually P0595 Enter comes into play,
so this paper adds a way to detect
whether the current evaluation is a constexpr evaluation.
And this is a expert-friendly
I would say even expert-only feature.
It seems like
it looks like a simple feature but it's not.
It's really, really not.
It has a lot of pitfalls, but it's really necessary
if you wanna implement any non-trivial thing,
non-trivial standard container at compile time.
So what this does, like I said, is basically it allows you
to detect whether the current function is being evaluated
as part as a constant initializer.
And so for example, if you wanna make vector clear constexpr
you run into the problem that we have ASAN annotations
and debug mode annotations, like I said earlier.
And these annotations are inherently non-constexpr-friendly.
There's no way, they don't make sense
at all at compile time.
So, we can't execute them.
You have basically like three options here.
Either you give up, and you just say, all right,
too bad, vector clear is never gonna be constexpr.
Or you remove the annotations, but then you're pessimizing.
Well, you're not in that case, you're not pessimizing,
but you're making your implementation worse at run time,
which is the case that we almost always care about.
So we don't wanna prevent implementations
from having nice features, just because
we need to restrict ourselves to this set,
that constexpr support, so that's not really good.
Or you do what we're about to do,
which is you basically switch
on whether you're being evaluated at compile time.
So it's not pretty, it's not great,
but this is why I said it's an expert-only feature,
where expect your standard library implementer to use
that kind of stuff to give you the ability to not care
about whether you're in constexpr context or not.
So in standard vector, if we are in a
in a constant evaluation,
then we're just gonna skip all these annotations.
They're not gonna be enabled at compile time.
And so the way this works is a little bit tricky.
It's a little bit tricky, so
on line eight here,
I call F, and then F will call vector v.clear,
so vector.clear, and inside vector.clear there is
this is_constant_evaluated
call being made.
And the idea is because X is a constexpr variable,
the right-hand side has to be a constant initializer, and
and because of that
because it has to be a constant initializer,
the magic function is_constant_evaluated
is gonna return true,
and then we're gonna skip the ASAN annotations right here,
which means that we're not be running anything
that is not constexpr-friendly.
And so the expression as a whole is a constant expression.
In the case of line 11, Y does not require
a constant initializer.
I mean, surely the compiler can figure out
that all of this stuff is
it sees everything, all this stuff is it could be folded,
but the core language does not require the right-hand side
there to be a constant expression.
And because of that is_constant_evaluated will return false.
And so we're gonna have the normal run-time code there.
So this is kind of tricky, because at first
when I was shown that, I was like, well,
F in both cases is called the same way,
and I mean it seems like clearly the compiler should fold
both of them, but this is
not how is_constant_evaluated works.
So another common problem that we have is that constexpr
does not actually require compile-time evaluation.
So raise your hand if you've ran into that situation
where you're not quite sure whether it's being done
at compile time or not.
Yes, right.
So, it's a very common problem.
And it actually extends way beyond just like,
oh, I wish I knew whether it was done
at compile time or run time.
Sometimes, it's like if it
for some things that we wanna do with reflection,
which I'll get to later, it does not make sense.
It would be dangerous if anything leaked to the run time.
I'll explain that later.
So what we wanna do is have basically a function,
which is exclusively a compile-time function.
There can't be any code generated for that function, ever.
And so we're adding
a new
keyword, which means please constexpr, this, do it.
It's not a suggestion, it's not, maybe constexpr.
It's like, no, no, seriously constexpr.
And so the way these functions
we call them immediate functions.
And the way to think about them is sort of as being a
a kind of like a Unimacro kind of.
Basically, when the compiler sees square of three
on line five here, it just says, all right,
that's an immediate function.
Let me go and evaluate that right away.
So, it folds everything, it just does constexpr evaluation
and then it evaluates the function right there.
There is never ever any code generated.
And actually, if you try to call it with something
that is not a constant expression,
it's gonna say, oh, this is an immediate function.
I can't generate code for that.
I can't evaluate that at run time, that's a hard error.
- [Audience Member] What linkage do these functions have?
- What linkage do these functions have?
You can't take their address.
They don't exist at run time at all.
They absolutely don't exist at run time.
So this actually has some very nice benefits.
I think we expect those to
to have some very large compile-time benefits,
because there's no code gen, they're much simpler.
But they're also more limited, you can't take their address,
you can't do that kind of stuff.
(audience member applauding)
I'm seeing some support there, that's great.
And so to summarize the constexpr changes,
so basically what we wanna do here is expand constexpr
to support more use cases, that's the general trend.
And we wanna allow persisting data structures
to the data segment.
And we also wanna allow
well, we don't really want to actually, we kind of have to.
But we have to make it possible to sometimes say,
all right, constexpr is failing me,
so I'm just gonna say if I'm running inside constexpr,
then do something else.
So, that's also something that we're allowing.
Even though we don't really like doing that.
And then we also allow requiring compile time evaluation,
using constexpr-bang.
Not exactly sure that's the keyword
that's gonna stay, but we'll see.
So do I have any
do you have any questions on kind of this section?
I'll take just one or two.
- [Audience Member] So constexpr-exclamation-mark
forces functions to be evaluated at compile time.
I wonder why this design choice, except of an annotation
at the call site of the constexpr function,
to say that it should be evaluated at compile time?
- Right.
- [Audience Member] Why would I need to write the functions
twice, the same function twice to give this contract
instead of just specifying at the call site
that I want this function to be evaluated.
- Okay, so if you wanna allow a function to be called
both at compile time and at run time,
then you have a way of doing that, use constexpr.
That's already what it means.
This is for functions where you must absolutely
evaluate them at compile time.
And the reason why there is no annotation at call site
is because the compiler needs to know that
that kind of function is special, and
it can't know that if it's not part of the declaration
of the function, so we need that specifier
to be part of the declaration of the function.
Nico, and then I'll keep going.
- [Audience Member] Louis, thank you.
I'm a little bit confused, because when evaluating
the new trait to find out whether
this is a compile-time issue, you use a run-time if,
so it seems that the run-time if is evaluated now
at compile time,
if the content is special,
the expression is special inside this.
That sounds like a contradiction to me.
This here? - Yeah, yes.
- Okay, actually, this is tricky.
If you use a if-constexpr, you're gonna introduce a bug.
This is why I said it's an expert-only feature.
If you use if-constexpr, suddenly
is_constant_evaluated is used in a place
where a constant expression is required,
so it's always true.
So, you need to use a run-time if,
because then the compiler is gonna check,
am I as a whole, am I a part
basically is there a root of the evaluation that is
that requires a constant initializer.
If you do that in
so for example, if you use is_constant_evaluated
in a if-constexpr or to initialize a constexpr-variable
in that function, or as an array bound
or something like that, anywhere
where a constant expression is required, it's always true.
So that's why I need to use a run-time if.
So the way to think about this is like, I might be
like vector clear might be running at run time
or it might be running at compile time, I don't know.
I'm gonna use the run-time if here,
which might actually be running at run time,
at compile-time rather, if I'm in a constant expression.
And so you just use a normal run-time if.
You don't use if-constexpr.
Really quick.
- [Audience Member] So, my question is C++ is already known
to be slow to compile, and now we are
embedding an interpreter for managed language
that appears to be very similar to C++.
So do you foresee any performance impacts on compilation?
- Okay, so
this is actually gonna improve compilation times
tremendously, because instead of
using template meta-programming to achieve these goals,
we're gonna use basically a constexpr, which is much better.
So, template instantiation is a much worse interpreter
than the actual constexpr interpreter.
I'm gonna have to keep going.
You can ask the questions at the end.
- [Audience Member] Okay.
- And so now that we have a bunch of stuff
to do constexpr computations,
we wanna be able to speak to the compiler.
We wanna be able to query information from the compiler.
And so basically what this means is
just extracting information about types.
We can already do that, even though we don't
often think of it that way.
We already have some reflection.
We have sizeof.
It's not fancy reflection,
but it is some kind of reflection.
You have a type, and then you query
some information about it, what's the size.
We can also ask for things like the alignment.
In C++11, we have type traits, so you can ask things
like, are you an aggregate, are you a POD,
are you a pointer, things like that.
So we do have some level of reflection here, already.
But we're limited, and the way we're limited specifically is
in what kind of answers we can retrieve.
We can only retrieve
we can only ask questions where the answer is
basically a primitive type.
And if you think about it,
the reason is because we don't have a way
I mean, before these constexpr improvements,
we didn't have a way of representing that.
So if somebody came to the committee and said,
"Hey, I wanna add a type trait that returns a list
"of the members of a struct."
Immediately, the question would've been like,
all right, I'm interested, but how do you represent it?
And before now, we didn't have a really good story for that.
Obviously, with the changes that we're making to constexpr,
this answer is gonna be different now.
And so let's talk about reflection a little bit.
So the Reflection TS.
And first of all, a TS is a technical specification.
The way to think about it is basically a feature branch
for the standard, so we have the main trunk
for the standard, which progresses, and then we
fork off feature branches basically,
and we work on features, and eventually we merge them
if everybody is happy.
That's kind of the way to think about this.
And so we've been working on a Reflection TS
for a couple of years.
And the purpose of the TS is to figure out
what the query API should be.
What shouldn't we be able to ask the compiler?
We don't wanna figure out the specific implementation
of that API, we just wanna figure out
what the API is, not really the nitty gritty details
of what the implementation is.
And because the TS started several years ago,
when we didn't have any plans to
we didn't know that we were gonna go the constexpr way.
Everything is based on top of template meta-programming
for now.
And so an example of using the Reflection TS, here.
We're extracting members, so I used this new keyword
called reflexpr to extract a type that represent
a meta-type that represents basically the type of Foo here.
So this MetaFoo on line six here is a magic type
that is essentially implemented as a pointer
inside the compiler's AST.
So it's basically a pointer to the AST node
that represents Foo inside the compiler.
But it's a magic type, you can't name it really.
However, that magic type has some meta-functions
that can be applied to it.
Here, for example, I'm using get_data_members_t,
and that returns another magic type.
There is a lot of magic here.
It returns another magic type,
which represents a sequence
of meta-types representing each member.
So, it's a little bit like a tuple under the hood.
You can think of it as a tuple
basically containing meta-information
for the members of the struct.
And so just like a tuple, I have some meta-functions
that I can use to extract individual elements,
so get element here, I get the first element.
And I get back a MetaX,
which is essentially a meta-type representing
the first member of a Foo.
And that is not int, that is some magic pointer
into the AST, that represents the int, that specific int.
And then I can query that meta-type,
extract some information, and finally I can
actually get the underlying type that it represents,
which is an int, actually.
That's a little bit how it works.
And so if we wanna print an enum here, we can do that.
It's a little bit tedious because
it's template meta-programming, but it's totally doable.
So, I'm not gonna go into much details here, but basically
I reflect on my color enumeration on line four,
and I get back a magic sequence of enumerators.
And then I create some fancy helper lambda,
using template lambdas here.
And what this lambda does is it basically calls
another lambda, I said I loved lambda, didn't I?
And so it calls another lambda inside
with each index in order, so I'm using basically
I'm calling the lambda inside a fold expression,
with the comma operator, on line 11.
And so this expands basically to a bunch of ifs,
one after the other, with each
for i, starting from zero to N,
where N is the number of enumerators.
So I'm expanding ifs, basically that's what I'm doing here.
It's a really fancy way of expanding ifs.
And then I call this helper function with the number
of enumerators, like creating an index sequence
with the right number of enumerators.
So it's a little bit fancy but it can be done.
And so there is a bunch of other features,
like you can get the location where a type
or an entity was defined.
You can get, like I said, the member types,
the base classes and such information.
So we're also thinking about adding reflection on functions,
like you can get the parameters,
and the return type and things like that.
There's also plans apparently to add reflection
on arbitrary expressions, which is cool but scary.
So, you could, for example, have an expression
and then you say, reflexpr of that,
and it gives you the AST of that expression,
and then you can walk and do crazy things.
I'm not exactly sure when or whether it's
actually gonna happen, but I've heard some plans
to do that eventually.
For sure, it's not in the NBB.
And the important part here is that syntax is
actually not settled yet.
So, what we plan to do with the Reflection TS is rebase it
on top of the constexpr syntax.
So, what we wanna do is instead of having reflexpr
return a type, we want to have it return an object,
a constexpr object.
That constexpr object under the hood is
just the same pointer to the AST node,
but it's represented as a constexpr object.
And once you have an object, you're good.
You can use a normal C++ syntax,
which is much better than template meta-programming.
So, for example here I can get the list
of data members as a vector.
I'm using a constructor-template argument deduction here,
so I don't have to type the type inside my vector.
So I get a vector of my members, and then I can index
my vector just like I'm used to,
so I get back the meta-information for that member.
And then I can use normal methods
just to query properties about it.
This is like type traits 2.0.
And then if I actually want to go back to redefine.
If I wanna go back from the conceptual world,
back into the type system, I need another keyword
to do that, that's why we need to introduce unreflexpr,
which is another keyword that brings basically redefines
the meta-information represented
as a constexpr object back into the type system.
So, you lift it down
you pull it down into constexpr, do calculations,
and then you lift it up
back up into the type system, with unreflexpr.
So with this syntax, printing an enum becomes much better.
Your out of template meta-programming there.
So I get the set of enumerators,
by reflecting on my enumeration.
And then I can iterate using a range-based for loop,
a little bit of a fancy range-based for loop.
I'll explain what this is.
And that's it.
And so this for, that thing here is not a normal for loop.
It's basically
it's basically a spanning for loop.
It spans out, it unrolls itself essentially, and
and the interesting part is that at each step in the
each buddy that gets spanned out can have a different type,
if you want, for the
for the loop variable, which means
that you could actually use a tuple in that place.
And this is actually quite important to realize that
this is not a normal for loop, because it's closer
to template instantiation than it is to actual
a normal run-time for loop, because you could have
a different at each step in your for loop.
So the status of reflection right now is, like I said,
that basically the
we're trying to figure out what the compiler query API
should be.
We're trying to figure out
what happens, for example, when you have a function
which is really re-declared and your asking
for the parameter names.
Which ones do you get, these kinds of questions,
which are very important to get right.
But we're not so concerned at this very moment
with the exact syntax that we're gonna end up with.
We expect that we will rebase on top
of the constexpr work, when it is ready.
So, don't go crazy if you see the Reflection TS and you're
like, what is this template meta-programming syntax.
I want none of this.
We're just making progress at this point,
and we will rebase on top of the syntax
that makes a little more sense.
And the goal again is to write normal-looking C++ code,
which runs at compile time efficiently,
without templates and instantiations.
Actually, this is driven a lot by compiler implementers,
that are tired of
me basically abusing their compiler,
to do template meta-programming.
They wanna put me out of business, so that's what that
the design here is really aimed
to produce an efficient model to do compile-time evaluation.
So now unto code injection.
We considered a bunch of alternatives,
like raw string injection,
programmatic API to kind of
bring back the
to inject basically code back into the compiler's AST.
And then we basically settled on something
that we call token-sequence injection.
What I mean by that here is that you extract information
from the compiler,
using the query API, so using reflection essentially.
And then you use constexpr to perform calculations,
computations, algorithms on
involving that data, and then what you wanna do is basically
influence the compiler's AST using the result
of these computations.
And to do that, like I said, we're settling
on token-sequence injection.
And I'm gonna be a huge tease right now.
I'm just gonna say go to Herb's keynote.
I apologize but I can't steal his thunder.
I believe he's gonna explain that in much more details.
Unfortunately, I'm not gonna talk about that.
I'm also kind of out of time soon, so.
So to wrap up, I'd like to kind of give a bird's eye view
of what I think the timeline is for this stuff.
And I wanna say this is not a commitment.
This is just me, saying what I think is gonna happen,
but I'm being very optimistic.
This is absolutely not a commitment from me
or from the committee or from anything else.
Just to be clear here.
It's just I think it is reasonable to expect that
that kind of timeline is gonna be respected, we'll see.
So I think in C++20 we're gonna see more constexpr,
because most of the papers that I presented today,
actually they're well
they're pretty advanced in the committee anyway,
or some of them have been merged already.
And we don't foresee a lot of problems with them,
so we think they're gonna make it.
So I think standard vector, standard string,
maybe standard map if we get the time,
they're probably gonna be constexpr in C++20, hopefully.
And the language features that are required
to make these containers constexpr as well,
so like I said, try-catch, allocations,
the promotion to static storage.
We've got virtual calls now too, a few things like that.
And so I think we're gonna see those in C++20,
that's pretty safe to expect that.
Then we might see an experiment
like we might see an experimental reflection facility,
which I'm not exactly sure whether it's gonna be built
on top of the template meta-programming syntax
at that point, or whether it's gonna be rebased already,
but we might be able to provide that kind of stuff for '20.
That's not too sure, but we might.
In C++23, however,
we should be able to have a reflect
a proper reflection facility that is based
on top of constexpr.
Again, not a hard commitment.
I'm just expecting, looking at how things are progressing
that we might be able to make it, we'll see.
And we might have some code injection mechanism,
although that is kind of a little more complicated.
So go to Herb's keynote, like I said.
This one I'm not sure we're gonna land for '23.
But for sure for '23, there is no reason
why we couldn't have a big, big chunk
of the standard library be constexpr.
Well, I think this is gonna change the face of C++,
really, I mean, this is huge.
This opens a lot of possibilities.
We can write really, really cool libraries with that.
Imagine JSON libraries that just spit out JSON
without you having to do anything at all, that's super cool.
Or like serialization, de-serialization,
all these things are gonna be much simpler.
We can increase code reuse a lot, I think.
We can stop writing boilerplate all the time.
We can also make interoperating
with other languages much easier.
Imagine what it would look like to generate Python bindings,
for example, using that kind of facility would be
much easier than it is today.
However, it also opens up really, really nice possibilities
for shooting your foot, and that's kind of bad.
But we're C++ programmers so we like to think
that we have control.
And never forget that with great power comes a lot of fun.
(audience laughing)
So anyway, none of this work would be possible
without a bunch of people, and this is
really just a subset of the people that are working on that.
There's a lot of people involved.
And we're really doing our best
to bring that kind of facility to you folks, and
I think we can make something good with C++, thank you.
(audience applauding)
So I'll be taking questions.
Please come with the mics.
- [Audience Member] Will constexpr-exclamation-mark
work on constexpr variables, as well as functions?
So that you don't have to say inline or define them.
- Constexpr variables, like constexpr-bang, some variable.
So we thought about that, there are
some pretty nasty difficulties with that having
to do with, I think, they're linkage.
That story is not really easy to figure out,
so for now we're leaving them out.
This is covered in the paper actually,
but we thought about that, and it's just not
it opens up a can of worms,
so we're kind of skipping that for now.
- [Audience Member] Thanks.
- [Attendee] So today in some of the talks
we were recommended to like--
- Can you speak closer to the mic, please?
- [Attendee] Make everything constant,
like everything kind of moves into constexpr.
Should we consider making everything const by default
and making stuff mutable only when it's needed, mutable.
- Right, should we switch to default?
Well, C++ is the language that gets the default wrong.
Should we make everything constexpr by default?
I don't think so.
Constexpr is actually a
constexpr is
when you say constexpr in front of something,
you're putting a barrier on the implementation.
You're saying, I will never implement that function
or that functionality in a way that can't be evaluated
at compile time, so it's actually some
you're actually stating a contract,
which is why I would personally
I think I would oppose to, for example, having constexpr
constexpr-ness be deduced automatically by the compiler,
that kind of stuff, because suddenly
you would be promising way more to your users,
say if you're writing a library than you are currently.
So, I think saying
I think white-listing things as being this is good
at compile time, this is good, this is not.
I think it's still a good thing, at least for now.
We'll see whether that changes in the future,
but I think for now it's safer to go that way.
- [Attendee] I think this will be a short thing, given
the question that was just asked.
Kate Gregory earlier mentioned a method
of finding correct constness in your codebase
by adding const to everything,
and then starting to leak things.
Should we now be doing that for constexpr also?
I think you're just gonna say the same thing.
- Yeah, I don't think it's a good idea.
I think it's better to selectively enable constexpr,
but you know,
at that point.
Honestly, I think that the kind of stuff you do
with constexpr today is too limited
for us to have a really, really clear
we don't have a lot of experience with what it would be like
to take a whole codebase and be like,
actually, how about I run my whole application
at compile time?
We can't really do that today,
so we might get closer to that, and in that case,
we'll get some experience and we'll know whether
it makes sense to span constexpr literally everywhere.
For now, I think it's much better not to do that.
- [Attendee] Hi, this idea came to mind
when you had your slide on challenges with reinterpret cast,
with the small buffer optimization, nested string.
For a string being constexpr or not,
and so would it be reasonable to consider having
an overloaded class set based on if it's constexpr or not,
so you could have different behaviors, if you're operating
with that class in constexpr versus not,
versus saying there's only one implementation
that can be used in both contexts.
Does that make sense?
- You mean different types--
- [Attendee] 'Cause small buffer optimization makes sense
for run time reasons when you have performance reasons
at run time, versus at maybe compile time
I have different things I wanna optimize for.
Still have to be the same class, same name,
because in a constexpr function that maybe is run time
versus maybe used at constexpr,
I wanna use the same class, same interface,
but have different implementation--
- Yes, so actually this what we plan on doing,
for strings specifically
we're not gonna give you constexpr string.
We're just gonna give you std::string.
It's just gonna work magically in both cases.
And the way we're gonna do that is we're gonna say, oh,
I'm being run in a constexpr context.
I think this string does not fit
in my small buffer optimization.
So, you just never use the small buffer optimization.
- [Attendee] But then what about types I implement?
Like if I wanted a class, my class,
and then a constexpr class, my class,
and do that all myself, and have it
not be inside the compiler, for only standard library types.
- I don't think that would work,
because you can't change the type based on
whether you are evaluating--
- [Attendee] The question is what's your opinion
of such a proposal?
Is that a worthy proposal they could at least even consider?
- I think I would need to sit down.
I think I would need to sit down and talk about this
more concretely to really understand
what the implications are,
that's what I think. - You should totally do that.
- I think it's not necessarily a bad idea,
because I can see some uses for it,
but I think it would kind of break the model
of the C++ type system, basically.
So I think it's something worth considering, but yeah.
- [Attendee] So, a related question
but for functions, same way I can overload a function
on const versus non-const.
You can't do that with a type but you--
Closer to the mic, sir. - You're on constexpr
for a function versus non-constexpr?
- Can you repeat that, I missed that?
- [Attendee] Can you overload a function
for constexpr versus non
constexpr-bang versus non-constexpr-bang?
- We can't, no you can't.
But what you can do, however, like I said,
is basically say, oh, am I being evaluated
at compile time or not, and then act differently.
Although, you should try not to do that.
You should let me do that.
- [Attendee] And for the is_constant_evaluated,
what about in a
constexpr constructor that is not used
to construct constexpr variables, like std::mutex,
would is constexpr evaluated be?
- What about a constexpr constructor
that is used to construct non-constexpr variables,
is that what you said?
Yeah, I mean, but that work is varied.
You can have a--
- [Attendee] But what would the value
of the is_constant_evaluated?
- Oh, I see what you mean.
It would not be true, because
what you're initializing is not constexpr.
- [Attendee] Thank you.
- [Audience Member] Hey there.
So my question is when we're talking about doing
a lot more computation at compile time
as we expand constexpr, do we expect
that if we're doing really, really heavy
sort of pre-computations and stuff at compile time,
do we expect compilers to be able to somehow cache
those values in a way that it doesn't really,
really slow down our builds, tests, write cycle.
- Yeah, so there is kind of
today this is what happens with instantiation
with template instantiation,
everything is memorized, everything is kind of cached.
And that's actually a problem, because it means
that we use a lot of memory when we compile,
so I think
talking to some compiler implementers,
actually there is a big
they wanna step away from that.
They wanna be able to discard values
after they've used them.
They basically don't want things
to live forever in the compiler.
So I would say it's actually undesirable
to cache all, everything.
- [Audience Member] But what I mean isn't in one run.
What I mean is I run my compiler.
It does some computation, something else breaks,
I fix it, I run it again, and I don't wanna do
that same big pre-computation that I did the first time,
'cause I didn't change any of that,
just the code that I didn't change.
Is that something that's reasonable for us to expect?
- I see what you mean.
This is definitely not something that we have thought about.
It's kind of
this question applies to what we're doing today.
I would say we don't really have a solution
for that right now.
If this is something you can do, then you could hoist
everything into a separate TU,
and then you just compile that TU once.
But obviously if it's in a header,
then you need to redo the whole computation every time,
which is true today with constexpr.
And I mean, the solution for that would be really cool.
I clearly don't have one right now.
- [Audience Member] Cool, thank you.
- Sure.
- [Audience Member] I'm not sure that I entirely follow
the purpose of allowing try-catch within constexpr.
If throw generates a compile-time error,
then that would mean your try-catch is
inherently unreachable code, which you would want
to discourage, right?
- That's exactly the point.
The point-- - You allow it.
- Because I wanna make standard vector push_back work
at compile time, so the thing is
I'm fine if there was a hard error, a compile-time error.
If you actually throw something from within your push_back,
but for the majority of cases where you're not gonna throw
anything, I want it to be a valid constant expression.
So, as of this paper, basically try-catch is equivalent
to just a brace, just you're entering a scope,
that's what you're doing,
because it's impossible to get into the catch block anyway.
It's kind of a no-op.
Does that answer your question?
- [Audience Member] Well, yeah, I mean, my point is
because it's a no-op, then why not
just take out the try-catch.
What's the point of having it at all?
- Right, because it might not be a no-op at run time.
- [Audience Member] Oh, because the second proposal's
not included yet.
- Well, because if you call that constexpr function
at run time,
which you can, and then you run out of memory,
you want the bad alloc to
or you want the out-of-bounds error, for example, to be--
- [Audience Member] Okay, okay.
So then if you add the second proposal
to make a decision based on whether
your running is constexpr or not,
then you could actually
you could actually have logic in one case
and not in the other.
- I'm not sure I followed that.
- [Audience Member] Within the implementation then
of the standard vector, if later you add the ability
to say, hey, am I running within constexpr,
you could then choose to remove the logic
in the constexpr case, and not even bother with the--
- Right, yeah.
- [Audience Member] Okay.
- Yeah.
- [Attendee] For the C++ modules, do you expect actually
constexpr expressions to evaluated
when module is compiled or when
in the phrases interpreted by another module?
- That's a good question.
That's a really good question.
I believe it would be when the module is compiled.
Let me come back to you on that.
- [Attendee] So I'm gonna ask something
about the injection part. (snickering)
You're probably gonna slap my face all the way
up to the next keynote.
You mentioned up there some options and we seemed
to be going with tokens.
- What?
- [Attendee] For injection.
- Yeah.
- [Attendee] I don't know, the first thought that came
to my mind is wouldn't something, unreflexpr--
- Can you just speak closer to the mic, please?
- [Attendee] What?
- Can you speak closer to the mic?
It's really hard to hear from here.
- [Attendee] For injection.
Wouldn't something, unreflexpr, the natural way to do it.
Same as you can take real types and functions
and what not into a representation of an object,
taken just the other way.
- The problem of code injection is actually quite tricky,
because you have to generate new declarations
and then somehow find a way to inject them
into an existing declaration.
So unreflexpr is not sufficient for that.
- [Attendee] Okay.
- [Audience Member] So with all of this compile-time code,
how do I debug it?
- Right, big question.
How do you debug that?
You run it at run time, I don't know, I mean.
(audience laughing)
Honestly, I think we're
the tooling for C++ is gonna have
to kind of follow these changes.
It's kind of the same answer as for
when Herb is asked questions about meta-classes,
for example.
We're adding new stuff to the language,
and we have to be responsible.
But also the tooling has to follow.
So I mean, there are some tools that allow you to,
for example, step through template instantiations.
One of them is called templite.
And so I think it's about tooling there, really.
- [Audience Member] Okay.
- [Attendee] In the case of the constexpr new.
- Can you speak closer?
- [Attendee] In the case of constexpr new,
what does a delete do, is it a no-op
or does it actually do something in the compiler?
- In the case of constexpr what?
- [Attendee] Constexpr new.
- New.
- [Attendee] Yeah, memory allocation in constexpr,
which you need to make.
- It actually allocates
the compiler is actually gonna allocate memory for you.
- [Attendee] For delete, does it actually delete
the memory or is it a no-op, or--
- That's gonna depend on your compiler implementation.
I expect some compilers might say,
I'm not even deleting stuff because I'm just gonna free
all of my pool of memory when I shut down,
but some other compilers might decide
to actually free the memory.
- [Attendee] So could you potentially
would array be able to do the same thing
in place of the new?
So you could just use an array.
- In place, new.
- [Attendee] Instead of a new.
Instead of using a new,
could you use array, because an array is
just allocating memory, it's usually on the stack
at run time, but--
- The problem is you can't use
you can't just allocate memory like that.
You can't
the one thing we wanna avoid, like I said earlier,
is to give you the ability to shoot yourself in the foot,
in terms of like, oh, allocate for that type
but in place newing with a different type,
that kind of stuff, this can't happen.
I think I'm gonna have to cut it here,
because the next session is
we can talk offline after.
- [Attendee] Just to follow up the previous question
that was made here.
Is there any proposal for ability to
to have
I don't know, emit warnings or wait till
actually get outputs off the constexpr?
- I'm sorry, I can't hear you.
It's really hard.
- [Attendee] Is there any proposal for emitting warnings
or getting any kind of log
out of the constexpr?
- No, no, the committee usually doesn't
we usually don't standardize that kind of stuff.
This is gonna be implementation dependent.
So your compiler is probably gonna try to be helpful there,
but it's not the kind of stuff that we write proposals for.