What Makes Modern C++ Compelling For Programmers? - Gareth Lloyd
By ACCU Conference
Summary
Topics Covered
- C++ Delivers Modern Ergonomics
- Chrono Evolves to Full Calendar
- Operator Overloading Enables DSLs
- Constexpr Shifts Runtime to Compile
- Format Strings Checked at Compile
Full Transcript
So I'm G. F L F L F L F L F L F L F L F L F L F Lloyd I actually run these AO York meetups. I'm also a senior software
York meetups. I'm also a senior software developer at a database company called Membr. And this is my little talk called
Membr. And this is my little talk called is that C++?
So at the beginning of the year I wrote this bit of code and all it does is print out all the dates and times for all the Aku York meetups happening this year. And so to answer the questions at
year. And so to answer the questions at the beginning of the slide, is this C++?
The answer is yes. And you know, some of you in this room have never done C++ and you're like, so what? Others of you in the room have seen C++ before. And
you're going, wait, that can't be C++.
That's way too short. Uh what what the hell is that feature? I've never seen that before. Um but this is, you know,
that before. Um but this is, you know, code that I'd expect somebody who's up to date in the C++ language to be able to write. And it's actually quite
to write. And it's actually quite concise, ergonomic in my opinion. So I'm
going to actually use this as an example to talk through some aspects of C++. So
for if you're not familiar with C++, it will actually probably tell you one or two things about it that you didn't know.
So let's start off. First of all, C++ is a strongly typed language. All right, I get that it has a history with C and C has a lot of implicit conversion. It's a
weakly enforced language. But C++ is a strongly typed language and weite kind of avoid all those implicit conversions as much as possible in order to make you know robust libraries that you can use.
So this is true in many programming languages. You do not want the integer
languages. You do not want the integer 2025 to represent years. You actually
want a strong domain type. So this will actually be stood chrono year. Um but
you know that's a really big mouthful thing to construct and type. So actually
there's a really good ergonomic feature within C++ of userdefined literals and you can do it for you know arbitrary types uh but this one is for stood chrony year and it has a suffix y. So
you get this nice ergonomic way of constructing with no real overhead. It's
just um nice syntactic sugar for constructing a stood chrono year. I've
kind of like jumped over a bit of C++ history because it's not like C++ came out you know with every single feature that you wanted straight away perfectly.
So in C++ 11, yes, we did get chrono, but when it came out in C++ 11, what you what you got given were um like clocks and time related things. So you could
ask a clock for a time point and you could actually play around with like hours minutes seconds milliseconds nanconds, etc. And they're all about duration. And you got all the methods
duration. And you got all the methods around these types such that it's stopping you from doing the incorrect thing. So I can take a duration and add
thing. So I can take a duration and add a duration and get back a duration.
perfectly fine. I can have a time point and a duration and get back a time point. Also fine. If I try to take a
point. Also fine. If I try to take a time point and add a time point, that's a compilation failure. So using strong types, and this is not true just true for C++, it's true for many languages,
using strong types to prevent you from uh doing the wrong thing.
And it wasn't until like C++ 14 that we actually started getting the userdefined literal in. So we've got the time
literal in. So we've got the time literal. So now you can actually in a
literal. So now you can actually in a very ergonomic way spell out 18h + 30 min. And that tells you 6:30 p.m.,
min. And that tells you 6:30 p.m.,
right? And then eventually after many years, we get to C++ 20 and we actually get dates. So all the calendar stuff. So
get dates. So all the calendar stuff. So
we can start talking about years, months, and days. And in fact, if you take a look at like line 15, uh, line, no, line 17. uh we can talk about the
first Wednesday of a month which is very different from like on line 25 where we turn it to local days and that actually turns it into for example today
September the 3rd an actual date and we can still like add that onto a duration to get a time and that time isn't based anywhere in the world. So in order to actually say that this meetup exists
somewhere here in York let's tie it to the Europe London time zone and so we actually have zone time. So you've got all these strong types making sure that you can do everything correctly.
I kind of already hinted at this, but I'll explain it in a bit more detail. So
C++ has overloading. What this means is that you can have multiple functions with the exact same name and their different implementations. And the way
different implementations. And the way that you statically differentiate between them is the arguments that you pass in to that function. So if you had a function that took in an integer uh
called I know fu and then you had uh another function which took in a double called fu you could have two separate implementations for that and the overloading would pick the right one statically based on the types that you
passed in.
Operators are nothing special. They're
just another function. Um but they allow you to have like little syntactic changes. So you can actually have
changes. So you can actually have prefix, postfix or infix operators. So
operator slash is one of those infix operators and this allows us to have the ergonomics to be able to compose like dates and uh stuff with a nice concise
format. So we can say 20 25y/ February
format. So we can say 20 25y/ February and that's a very nice way to construct a year month.
The variable that we've assigned this to um we could actually spell it out in a very long form. We could say uh stood chronoy year month. But you know that's noise again. So in order to keep things
noise again. So in order to keep things nice small and concise, we can actually use placeholder type specifiers which also came out in C++ 11.
Um this isn't like some magic variable that can take anything. It is determin deterministically uh done by the compiler that what's on the right hand side of the expression it is a stood
chronoyear month and that variable will only ever be a stood uh chronoyear month.
So I'm going to get uh a bit of a side step away from this code to show you a few more things that have happened over the years in order to make the ergonomics of C++ better.
So pre C++ 11 um the standard template library is brilliant. It's wonderful. We
have generic containers and we have generic algorithms. Um but in order for them to work together there's a concept in C++ of iterators and that is a like an abstract way that they can
communicate to each other to say this is how the container is navigable and how do you read elements out of that container.
Um but PC++ 11 you'd have to spell out that iterator type in its full form. So
you go stood vector int con iterator.
That's a mouthful. So already you can see just by using auto it makes this simpler. We know that CB begin is going
simpler. We know that CB begin is going to give an iterator. We know this is a C++ language and we know about iterators. We know that's going to be an
iterators. We know that's going to be an iterator.
This is also a very common iteration pattern. You're going through a
pattern. You're going through a container and looking at everything in that container. So why not have some
that container. So why not have some syntactic sugar for it because it's so common and that's what we've got. So we
have this rangebased for loop and now we no longer need to actually specify anything about the integers. It's all
hidden away syntactic sugar behind this.
Uh so we still have the container that somehow behind the scenes is getting those iterators. We're going to iterate
those iterators. We're going to iterate through those and dreference it to get our value out of it.
Whilst we're talking about cleaning up code, if you take a look at line five and six, we are constructing a vector and then we're actually going to push back a value onto it. And that seems a
bit, you know, bit too much. Two lines I know is too much, right? Um so what we can do in C++ 11 is actually tell it these are the values that I want you to construct at construct time. Okay so I
can say with an initializer list which all of that initializer list contains is the value 42 at construction time.
Please populate that 42. So it does it all in one go. You'll also notice in the previous slide I had to spell out explicitly that this was a so vector is a generic container. It is a generic
type on there to say that all the elements inside of it are going to be intra. And I had to explicitly say that
intra. And I had to explicitly say that here. On the next slide, I'm using C++
here. On the next slide, I'm using C++ 17's class template argument deduction in order so I don't need to do that. And
there's a bit of machinery going on. So
the initializer list is also templated and it's saying it's initialized a list of integers. And automatically at
of integers. And automatically at construction is tying this together to say, oh, I know what you mean here is that it's going to be a stood vector of integer. Again, making things concise.
integer. Again, making things concise.
For completeness, me being a performance freak in this little demo thing, I I have to say I'm going to use array instead. So, a vector is a dynamically
instead. So, a vector is a dynamically allocated array. It's on the heap. If I
allocated array. It's on the heap. If I
was going to do this in some production code, it'll be a stood array, which will do it here on a stack instead.
Okay, so I've already talked about a bunch of things that you can see in other languages, strong types, a bunch of features that make things a bit more ergonomic that you can actually uh have
smaller code and less boilerplate.
But what is it that draws me to C++? And
it's this idea that I can actually move stuff from runtime that doesn't have to be done at runtime. It's metaroming. I
can do that at compile time instead. So
what this const expo is doing is saying this variable that I'm initializing right now make sure anything on the right hand side of this expression must have been done at compile time. And what
this means is that all the code that comes after this that depends on first month, it can actually inline that value and have better codegen because of it.
Okay, so let's talk about a bit more evolution of C++.
This is what metaromming used to look like. This is known as template
like. This is known as template metaroming. And you you'll notice
metaroming. And you you'll notice compared to many other languages, generic systems. So C++, not only can you pass types in the generic arguments, we can actually pass values. Not many
languages can do that. Um there is restrictions on what values you can pass, but here we're just having an intervent. So at the bottom on line 17
intervent. So at the bottom on line 17 when I'm instantiating fib 10 and then getting out the value from that that goes to the the top template uh right at the top and that recursively
instantiates fib 9 and fib 8 and it keeps on recursing all the way down until we get to the template specializations fib 1 and fib 2 which have hardcoded their value to be one and this all bubbles all back up to say 55
at compile time.
Put your hands up if you think this is code that you want to write on a day-to-day basis. Right, there you go.
day-to-day basis. Right, there you go.
No, no hands were shown.
So this is how we do it today. And so
this is a constexer function. So it
looks like normal C++ apart from all we've done at the beginning. We've put
const expert. And what this basically means is this implementation of fib we can actually run at both runtime and compile time. And to do that we've
compile time. And to do that we've actually said um we've got some restrictions on what we can do inside that function. For example, you can't
that function. For example, you can't allocate at compile time and deallocate at runtime. That makes no sense. So
at runtime. That makes no sense. So
context is protecting you from a whole bunch of undefined behaviors etc. So on line 11 when I have this context per variable it's forced that that fib 10 must have happened at compile time. So
this is the way that we do uh meta programming today, right? So all this has built together to
right? So all this has built together to this big thing here. So what we're going to do is, you know, I say it's big, it's it's printing out a string, right? Uh
but we're actually going to tie uh this whole idea of having some ergonomics, some uh moving stuff from runtime to compile time and try to do some good code. So what's happening here? We've
code. So what's happening here? We've
got print which is also got a format string. So it's not actually a format
string. So it's not actually a format string straight away straight away. We
this is one of the few cases where you actually get a little implicit conversion that is actually cringing literal that gets implicitly converted to a format string but it uses stuff that print is saying in order to make
that work. So it's found in uh that
that work. So it's found in uh that format string that there's two placeholders, right? And both of those
placeholders, right? And both of those placeholders are referring to the zeroth uh argument. So print has play said you
uh argument. So print has play said you know these are the arguments there's only one of them so in the zero place there is something great at this point we do not get a compile time failure there is at least the zero argument for
us to do our formatting with the zeroth argument meetup happens to be type zone time so again the format string is going to go away and say is there a formatter for zone time and yes at compile time it
can find a formatter all good it's then going to say right I'm now going to pass this at compile time percentage b% percentage D, percentage R, percentage Z. Do those formatting options make sense? And it goes yes,
wonderful. So, it's going to make some
wonderful. So, it's going to make some highquality code that can be run at runtime, which will extract only what's needed out of the Meetup variable for because it's a zone time type and then efficiently fill in your print buffer
with whatever is needed to do that at runtime. So, this is a very efficient uh
runtime. So, this is a very efficient uh code gen going on from this.
Okay, so I just talked through that bit of code and hopefully almost every line um should make sense to you now. Um
yeah, sure, many languages have strong types. Many languages have a lot of
types. Many languages have a lot of ergonomic features. Uh fewer languages
ergonomic features. Uh fewer languages have the ability to move stuff from runtime to compile time. And that's one of the major reasons that I have for using C++. Um and even if you aren't
using C++. Um and even if you aren't going to use C++, never stop learning.
This stuff can be just a little thing on the side of your brain going actually that's a different way of approaching this problem.
This is what it actually printed out.
And as you can see, we have September the 3rd, uh, 18:30 British summertime.
That all worked out nicely. And this is my pitch to say, "Please come along to the, uh, meetups in the rest of the year." There's one in October, one in
year." There's one in October, one in November, and one in December.
Uh, I do actually have a couple more uh, little bits of C++ code that I can show if there's no questions. Um, because
these are things that exist in C++ that you might not find in many other languages. So, let's first of all, any
languages. So, let's first of all, any questions?
First of all, did did I understand right that um you you showed format strings are checked at compile time.
>> Yes.
>> If you write that format string wrong, it won't compile because that's really cool. I might become a C+.
cool. I might become a C+.
>> So if I put percentage f in this, I get a compile time failure.
>> I'm over cotlin now.
Second question. Um, do do you think that um operator overloading can go too far? I mean, some of those operator
far? I mean, some of those operator overloads I look at and I think the slash for the year, month, day, and the indexing operator for the first Wednesday of the month, it's not
entirely intuitive because that's not doing what the operator would normally do. It's not doing division or array
do. It's not doing division or array indexing. Do you think that's a valid
indexing. Do you think that's a valid criticism of kind of going so far down this route? But this is true in many
this route? But this is true in many languages where you down in a like a domain specific language like you are reusing symbols and tokens in order to make something ergonomic within that
domain. And I don't think this is a bad
domain. And I don't think this is a bad ergonomic decision for saying, oh, rather than having to say dot um add month, dot add day, like it's an
alternative syntax. And
alternative syntax. And it it may be obvious to you or it may not be obvious to you, but it I think it depends on what the the code domain you're working with. If you're doing
stuff with times and dates all the time, this is going to be like the way you want to code. If you're not, then maybe you want to do the more verbose way because it's not that common in your codebase. Therefore, you want to spell
codebase. Therefore, you want to spell it out in a long form with constructors and with uh you know dot method, etc., etc. >> That's a fine line, isn't it? It's
sometimes you step over. If those were paths, would you complain for example?
>> I mean, I I don't know if I'm necessarily complaining. I'm just
necessarily complaining. I'm just curious where where the line is.
>> Point is, it was you'd recognize it much much more.
>> Yeah. Yeah.
>> Yeah. I mean, have have you have you seen code where you think, "Oh, they've gone too far. That operator doesn't make sense."
sense." >> I mean, the the the first complaint in all of C++ Yeah, the first complaint with uh C++ history is the first big usage of this was um streams. So, right
now we we're now using like print. It
used to be that you would streaming things in or out and you'd have like the bit shift operator, the the chevrons and that was like practically the only usage that it was within the standard library.
It was weird and people didn't like it.
Um, and there's other libraries which I think go too far. So I think there's I think Boost Spirit comes to mind. Yeah,
I'm getting a nod. Um, so yeah, Boost Spirit really goes to town, but it's trying to again it's a DSL. It's trying
to um make a particular domain take less code to type. Um, yeah.
Anybody else want the mic?
uh more an observation I find funny out of all this like really nice and quite complicated syntactic sugar and stuff that's making everything really complicated things in a really difficult
way make it simple is it true that the most recent and futuristic thing that it was on everything you explained was the print statement in 23
>> yeah that's the last thing they made >> everything builds together so in in what I kind of hinted at here or you know where was it yeah Yeah. Uh the fact that
we can pass values in template arguments. Um over time uh you know it
arguments. Um over time uh you know it started off with just integers and uh other things which were very simple. Uh
and now we've got to an idea that anything that's a structural type that you know bitwise that that is fine. Um
oh sorry knocking my mic. Uh and that's what's needed in order to make that work. So that actually translates this
work. So that actually translates this value known at compile time into something that can be templated. Um
because what's actually happening is translating that into some other magic types. Yeah. But yeah, that's what it's
types. Yeah. But yeah, that's what it's doing.
>> Yeah, makes sense.
>> Cool. Right. I'll just quickly show you uh those couple of C++ demos just in case uh they spark your interest. So
this is using a different feature of C++ uh known as um a fold. So I can actually go uh again template thing these are all different numbers. I can go from one
different numbers. I can go from one multiplied by 2 to 3 4 5 6 to I. Uh and
so this is a fold expression happening at compile time. So I can actually do the Fibonacci know the factorial function at compile time in this way instead. Um it's not a nicer syntax but
instead. Um it's not a nicer syntax but it's a way of doing it.
And then other libraries that come quite nice. So
uh there's ranges library where I can actually zip two bits of data together and then I can sort over the zipped thing. And so I can do a projection just
thing. And so I can do a projection just over the first bit of the key and that's going to sort both this collection and this collection. And so by the time I
this collection. And so by the time I look at the uh data later on, it is expected and it becomes sorted. So I end up sorting both of these but by a key.
And I know there's libraries out there that do this, but I think the way that the C++ libraries are being built up uh make this nice going forward.
All right, thank you very much.
Loading video analysis...