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

Video thumbnail
Thank you for coming to the talk,
it's called Make Classes Great Again,
my name is Vinnie Falco,
I'm the author of Boost.Beast,
which is a HTTP and WebSocket library
that's built on Boost.Asio,
it was accepted into Boost on July 20th,
it will appear in Boost 1.66,
which is scheduled for release in December.
I would like to say a few words about Boost,
Boost is a wonderful collection of
peer-reviewed, high quality C++ libraries,
if you've ever used features of the standard library,
such as shared pointer or thread or mutex,
you should know that these features got their start
in Boost, they predate their appearance
in the standard library.
The authors published their code into Boost,
and that enabled them to get
valuable feedback on their classes,
they can fine-tune it with feedback from users,
users got some experience using the classes,
and then they were able to write up
a nice proposal to add it to the C++ language,
because once it gets in the standard,
then it's baked in, there's no changing it.
So it's a really nice way to make sure
that you've got something that's solid,
that's well polished.
It's the process that I think is the best
for getting things into the standard.
Now, during the Boost formal review process,
you submit your library and a bunch of people,
usually smarter than me, look at your code
and they might say some things,
hopefully they'll say nice things,
they'll give you a critique of your codes,
sometimes it can be very honest,
but no matter what, you're going to walk away
knowing more about your library,
getting a better idea of what you can do to improve it,
if it's a really good library,
then it'll be accepted into Boost.
Here's an interesting fact - more people have been
to outer space, than have gotten their libraries into Boost.
(laughter)
Okay.
So, during this talk, I'm gonna give you
a quick primer on HTTP, nothing too deep,
just a light introduction.
Then we're gonna create a message model
in C++ to model those HTTP messages,
we're going to define some concepts,
we're gonna write documentation on those concepts,
and we're gonna create metafunctions
to check the types that meet those concept requirements.
So, HTTP, HTTP is the technology
that powers the world wide web.
It allows computers to talk to each other,
it divides computers up into two categories -
you've got clients, and you've got servers.
Clients, you're familiar with,
browsers are clients, there are command line tools
that are HTTP clients,
and then we have clients that you might not think of
as an HTTP client, like this Samsung Smart Refrigerator.
If you run low on groceries you can order from Amazon,
now Whole Foods, and it uses HTTP.
The clients talk to servers,
server is a piece of software that runs,
you've probably heard of Apache, it's an open source server,
these HTTP servers run on a variety of hardware,
from data centers to small devices,
like this Linksys router, if you open up your browser,
you connect to your Linksys by putting in the IP address,
and that page comes up, so you an administer your router,
that's a small HTTP server.
So, in order for HTTP to take place,
the computers need to be connected.
The client in this example, in the browser,
they put in the domain name,
the client performs a DNS Lookup to retrieve its IP address,
and it makes a connection to the server.
So, now that the connection is established,
the HTTP conversation can take place.
The client sends a message to the server,
that message is called an HTTP request.
The server processes the message,
it does its thing, and then it sends back
something called an HTTP response.
So, an HTTP request looks like this.
Now, the first thing that you notice is that it's text,
it's a readable protocol, it was designed a long time ago,
there's some words in it that you might recognize.
That first line is called the request line.
It contains the verb, GET, the thing that you want to do,
then the target, index.html, that's you wanna get that file,
presumably, and then a version, which is useful.
After the request line, we have some name value pairs,
called fields, and again it's text.
You can see User-Agent is the name,
and then there's a colon, and then Chrome is the value.
Now, the field names are case-insensitive, that's important,
we're gonna come back to that later.
So, that's the request, a response looks very similar,
again, it's text, we've got the first line,
it's called the status line,
it's a little tiny bit different,
it's got the version and then a numerical code,
200 means OK, which is like an error code,
it means everything went well,
there are other numbers like 404 which means not found,
500, server error, and then we have a human readable
piece of text that mirrors the error code,
that's for people to help de-bug,
it's not really used very much,
after the status line, again we've got some fields,
we've got the name value pairs
and the name can be almost anything,
the values can be anything,
it's really defined by the semantics of the message.
In this case, we have something called the body.
So this is a body of the message,
any HTTP message can have a body,
it's optional, doesn't have to be there,
but this one has a body, here it consists of text.
You can see some HTML that's in the body,
it could be binary or it could not exist;
it all depends on the semantics.
Now, at this point, the object-oriented minded folks
in the audience are probably thinking,
well hey, requests and responses are just special cases
of a more general concept called a message,
and you'd be right.
So, an HTTP message consists of the start line,
which is a little bit different for requests and responses,
then the fields, which are the name value pairs,
and finally an optional body.
The start-line and the fields are collectively
referred to as the header,
and the rest is called the body.
So, what we wanna do, we would like to model these
HTTP messages in C++, presumably using some type of class.
We wanna be able to inspect the attributes of a message,
we wanna be able to change the attributes of a message,
now that we have our message container,
we wanna create some algorithms
that operate on the container,
we'd like to be able to serialize the container,
which means turns the in-memory representation
into the network format, that series of bytes,
that text that you saw,
and then we'd like to parse that message,
which means take the bytes that come from the network,
and turn them back into the in-memory representation,
which is in our class.
So, I'm gonna take a stab at making a message container,
here it is, this is a request,
you can see the elements of the message.
There's a version, we have a couple of strings
for the method and target,
and then we have the fields,
here I used a map, it's a map of strings,
we're mapping the names to the value,
looks pretty reasonable,
then we have the body.
Now, the body can be binary or text,
and standard strings are perfectly capable
of representing binary, you can put nulls in a string,
you can have a string that's zero length,
which we can use to indicate that there's no body present,
you can have unprintable characters,
string is a very capable container for this sort of thing.
So that's our request.
Similarly, we can create a response,
it's very similar, not quite the same,
there's two things that are different -
the status and the reason,
but otherwise it's the same thing.
So, now we've modeled HTTP messages, we're done!
Great!
Okay, what's the problem here?
Would anyone like to venture a guess?
What's wrong with these declarations?
Just think about what's wrong.
Okay, so if you thought about any of the things on this list
well, there we go, so, the first problem
is that these containers are not AllocatorAware.
So, they're using the default allocator
which is probably good for most cases,
but for special cases, it's not gonna work very well.
We don't wanna just create a container,
we don't wanna create a good container,
we wanna create a great container,
and great containers need to allow you
to specify the allocator, otherwise certain people,
such as the folks in study group 14,
low-latency programming, they're not going to be too happy
with this container because they want
to specify the allocator.
The next problem is that the body is stuck at string.
Now that's a good choice for most of the time,
but it might not always be the thing that you want.
Maybe you want a vector, or perhaps,
if you work at a large company like Facebook,
you've got your own string
that's optimized for your own needs.
You can't use it here.
So there's not going to be too happy with this container.
Next, we have the map.
So, the choice of map, kind of arbitrary,
maybe we want an unordered map,
we don't have a choice because we've already made
that choice for the user.
Now the declarations that we saw do not account
for the case-insensitive nature
of the field name comparisons.
So, we've got that problem to deal with.
Finally, the request and response types that we saw,
they're distinct.
There's no relationship between those types.
So, if we want to create a single function
that is capable of serializing a message,
such as this function called write,
we have to make the parameter
for the message a template type.
So, this signature will allow us to accept a request,
it will allow us to accept a response,
but it will also allow us to accept things
that are not requests or responses,
like a "fu" or worse, an "int",
so the user can try to call that function,
the compiler will be very happy to match it
in the signature, but then it will probably
get a compiler error and a very long one at that,
and it's not gonna do the thing that we wanna do.
So, that's not very exciting.
The first thing we need to do is
refactor the declaration for this container,
to resolve the issue of the unrelated types.
So I've done that here,
I've created a class template,
template, that's where all the trouble starts.
So here I've got the message class template,
and you can see I've chosen to parameterize the class
on a bool, bool is a very logical choice,
it can have two states,
we've got two types of messages.
So depending on the value of the bool,
we'll have either a request or a response,
so now our strategy is to specialize this class
for requests and responses.
Here's the specialization for the request,
I've put "true" in for the value of the bool,
all I've done is taken the same fields
that we had before and just moved them in,
very simple and straightforward,
you can imagine the response is gonna look similar,
we're just gonna use the same fields.
Now, people don't like to see "true" and "false"
sitting naked in the code,
so we can create a few type aliases,
now we have request and response type aliases
to make things more friendly for the user.
Okay, let's look at our serialization function.
Now, the serialization function takes a message.
This is starting to look better.
If the user tries to pass a "fu",
the compiler will know,
you can't pass a "fu" to this function.
You can only pass a message that is either
a request or a response.
So, we're making progress.
Alright, next problem we have is,
our container is not AllocatorAware.
So, what can we change in this declaration,
to allow the user to specify the type of allocator
used for each of the elements, each of these data members
that needs an allocator?
Just think about the answer.
So, if you thought add an allocator template parameter,
hey, that's not a bad idea, let's see what that looks like.
Okay so this is that,
(crowd laughs)
so now, so, you can see
I've chosen to use
the same allocator for method and target
to cut down on the number of allocators,
it's a pretty reasonable choice,
you might those strings are gonna be roughly
the same size, not a bad compromise,
but we don't wanna use the same allocator
for the body, the body might be hundreds of megabytes.
Or, it could be tiny, who knows.
We don't necessarily want to use the same allocator.
Now, the fields are a problem,
because we have a container that wants
to allocate memory and the elements of
that container themselves want to allocate memory.
So, how do we tell this map
that we want the strings inside
to use a particular allocator,
scoped allocator adapter to the rescue!
Here's the scoped allocator adapter,
and it's gonna propagate the allocator inside
whenever the map creates elements, it's gonna work.
Now, show of hands, who thinks this is a great idea?
Okay, I don't see any hands up.
You're absolutely right!
This is a terrible idea.
If you're working at a company
and you're making this container,
your coworkers are not gonna be very happy with you.
They're not gonna be pleased.
If you're creating an open source library,
you might find you're not getting
as many downloads as you thought.
People are gonna think this is overly complicated.
Now, here's an interesting data point,
if you search for scoped allocator adapter on Google,
you're not gonna get a lot of hits.
Now, we need to be careful interpreting
the meaning of this data,
but I think it's clear that in terms of Google,
Google doesn't find many hits for it.
Okay, so we're just gonna put a pin in the allocator problem
it's very difficult to solve.
Maybe after we do some refactoring, we can revisit that.
Okay, so let's talk about customizing the body.
We want to allow the user to choose
different containers for the body.
This is one of my favorite quotes,
"All problems in computer science
can be solved by another level of indirection."
So, how can we modify this declaration?
How can we change this request container,
to allow the user to specify the type for the body?
I want you to think about the answer in your head.
So, if you thought, add a template parameter,
well we see how well that worked out last time,
(laughter)
but in this case, maybe it's not so bad.
So I'll just add a body template parameter,
now we'll change our string to be that type,
so now you can put a string there if you want,
you can put a vector there if you want,
so let's see how that plays out.
Here's our message class template declaration,
you can see I've added the body parameter,
okay, now it's in there,
here's our aliases, now instead of being normal aliases,
they are template type aliases,
they are parameterized on the body,
so the user has to specify the body
when they use the request or response aliases, not so bad,
if I have a request that uses
a standard string for the body,
I can assign a string literal, very nice.
If I have a response that uses
a vector of char for the body,
I can assign an initializer list of char,
okay great, now let's look at our serialization function.
I've changed the serialization function
to accept the message with the body parameter.
So now that function can accept any message,
no matter what body type it uses.
It's a very nice generic function.
So, you're probably wondering,
well how are we gonna implement that thing?
So, it's time for us to look under the hood
and figure out how that serialization function's gonna work.
So if you remember what I said earlier,
an HTTP message consists of a header, followed by a body.
Our strategy for serializing will be to first
serialize the header, we'll call this function,
we won't worry about that,
pretend it's written and already exists,
we don't need to get into the weeds on that, so that's done.
Now we're gonna serialize the body.
So, we call the standard "o stream write" function,
which accepts a pointer and the number of bytes.
And that's what we're gonna send that data over,
it's gonna output and it's gonna be great.
So if we have a request that uses a string for the body,
and we can call "write on "c out", it's gonna work!
If we have a response that uses
a vector of char for the body,
we can call "write" on "c out" and it's gonna work.
Now, if we have a response that uses
a list of string for the body,
we can call "write" on "c out"
and, hey wait a minute now, what are you trying to do here?
There's a big problem with this code.
Does anyone see what it is, anyone?
- [Audience Member] The data's not there.
No data!
There's no list data, what are we trying to do here?
And then, list size, that's not gonna do the right thing,
that's gonna tell you the number of items in the list,
not the number of bytes.
So, that was cool, the template parameter was cool,
but it's clearly not the one.
So, we need to fix it.
It might not be apparent, but there's a lot
of other problems with this approach.
For example, if we assign a string literal to the body,
that uses a string, we can call "write",
it's gonna work great.
But what if we wanna serve a file?
HTTP servers deliver files all the time,
it seems like useful functionality,
we don't wanna leave that out.
So maybe we wanna put the path name in the body,
and then have our serialization function stream it out.
But this is not gonna do the right thing.
You're just gonna get the name of the file, in the output.
Which is not what we want,
we want the actual contents of the file.
Okay.
We got more problems, so, if you look at this code,
I told you earlier that a body is optional,
so we can represent that with an empty string,
but even if the string is empty,
the size of our container did not get any smaller.
We're still paying for that standard string,
even though it's empty.
Now in C++, we have this mantra of paying for what you use,
so here we're paying for the storage,
even though we're not using it.
So that's not very nice, but it doesn't stop there.
If you look at the serialization function,
we're calling "write", but we're passing a size of zero,
so that call is completely unnecessary.
There's no body, but we're calling the function anyway.
Now you might think, hey, what's one function call?
But that's not how we roll.
We can't have people using other languages,
sneering down at us for our high level abstractions,
because they're paying an unnecessary run-time penalty.
We wanna have our cake and eat it too.
We wanna have a zero cost abstraction and that's how it is.
So, how can we fix it?
Think about the answer in your mind,
if you thought about adding another level of indirection,
you got it!
So, we're gonna add another level of indirection here.
Here's our declaration for the request,
you can see we have the body template parameter,
that's the type of the body,
how can we change this to add a level of indirection?
The answer is simple, rather than using
the body template parameter as the type for the container,
we're gonna use a nested type called "value type"
as the type for the container.
So now we're no longer instantiating the body type,
we're just using it to name some other type.
This is a little bit abstract,
so let's see how this plays out.
Here I've created a user-defined type called a string body,
it has a nested type called value type,
which is a standard string,
if I declare a response that uses the string body,
I can assign a string literal to it, okay.
Here I've created a user-defined type called a vector body,
which is templated on some element, "T".
It's got a nested type called "value type",
which is a vector of "T".
I can declare a response using vector body of char,
and I can assign my initializer list of char.
So, how 'bout the list?
Here's the list body, it's got the nested value type,
now we can declare a response that uses a list of string,
we can assign our initializer list of string literals,
but we still, we have a problem,
because the serialization's not gonna work.
So we've added a level of indirection,
but we didn't solve the problem.
What happened?
I thought we needed,
I thought adding levels of indirection helped.
Well it does, all we need to do,
is add another level of indirection,
we're gonna be rolling,
(crowd laughter)
so how do we do that?
Here's our serialization function,
you can see we're calling "os.write",
that's where the problem is, so how can we fix that?
So, we're gonna change it to call a static member function,
of the body type.
So what does this mean?
We're going to delegate the responsibility
for serializing the body container to the body type.
So once again, notice that the body type
is not actually instantiated,
we're just using a static member function
to solve our problem.
So now we just delegated the problem elsewhere,
in order to do that, we actually have
to have an implementation.
So here's a user-defined type string underscore body,
you can see I've added the static "write" function,
which does the right thing.
We send the container to the output stream.
Now, we can declare a response that uses a string body,
and it's gonna serialize correctly.
Can we solve the problem of the list of strings?
Turns out that we can.
Here's our list body, we've got the nested value type,
and now we have a static "write" member function,
which does the right thing for the list.
We loop over each element,
and we send it to the output stream.
Now, there's something really cool about this,
which is, this will not only work for strings,
but it will work for any type
that supports streaming to the output stream.
So, we just got a feature for free, it's very nice.
Let's revisit the problem of the optional body.
So, can we create a user-defined type called an empty body,
that lets us have zero cost?
Well, turns out that we can!
Here's our empty body type,
you can see we've got a nested value type
that has no data members.
Now we have our static write member function
it's in line, it's class definition is in there,
and it doesn't do anything.
So, when the compiler generates the serialization function,
and it calls the static function,
it's gonna optimize to nothing,
so we're paying no run-time penalty.
We have a little problem though,
the size of that body member can't be zero,
in C++ there's a rule, the address of any data member
of a class must be distinct from
the address of every other data member.
So, we still wanna pay nothing for this,
but we're gonna have to do a little bit of refactoring.
The first thing that we're gonna do,
is we're gonna eliminate the body data member,
and we're gonna replace it
with a couple of accessor functions.
These accessor functions will return a reference
to the body, const or non-const.
So how do we implement this?
So there's a feature in C++ called
the empty base optimization.
If a class is derived from another class,
and that other class has no data members,
then it does not add to the size of the derived class.
So we're taking advantage of that here,
we're deriving our message from the body value type,
we're using a private derivation,
so that the user can't see the derivation,
they can't see the hierarchy,
and now our implementation for the body member function,
just returns the body portion of the message, very simple.
Now, this adds a lot of boiler plate to the slides.
I'm showing you this to show you
that abstractions can be zero cost.
We're just gonna continue with body as a data member,
to avoid having too much code up on the screen.
Okay, let's revisit the file.
Now do we have the technology to serve a file,
simply by putting a file name into the body member?
Turns out, we can!
Here's our file body, we've got the nested value type,
which is a string, we've got our static write function,
only this time, the implementation of the function
opens the file, loops over the data,
and delivers it to the output stream.
Now there are two interesting things
about this implementation.
The first one, is that the body value type, up until now,
has been a container of bytes that actually get delivered.
But in this case, the meaning of the body
is now a file name.
The file name is not delivered, but the contents are.
So the semantics have changed.
The implementation of the "write" function,
up until now has been to deliver everything
that's in some container in memory.
But in this case, we don't have the entire
file contents in memory.
In this case, we're just delivering it four kilobytes
at a time, so that's a really cool feature
that came out with our design,
that's an indication that we're on the right track.
So, I have an announcement to make!
We've just created a concept!
That's right, body is a concept, so what's a concept?
If you have a given template type,
a concept will define the syntactic requirements,
in other words, what is necessary
for the correct compilation?
As well as the semantic requirements,
what do you need in order for the program
to have the correct behavior at run time?
All of the types that we've created
are instances of the body concept,
they meet those requirements.
Now, it turns out, everyone here
has already been using concepts.
If you've called any of these functions
in the standard library, such as find or sort,
the objects that you pass meet
certain standard requirements, such as,
input iterator, or less than comparable.
So, now let's talk about documentation.
That thing that everybody loves to write.
(laughter)
So, if a user sees our message container,
and they need to create an instance of this type,
chances are pretty good they're gonna know
what to do with the bool.
They've got a 50/50 chance of getting it right.
Hopefully they know they have to put true or false there.
But when it comes to the body,
now things are not so clear.
What do they pass for the body type?
If they're using the request or the response alias,
they need to provide a body type.
So how do we inform the user
of what kind of types that they can put there?
And if the user wants to create their own types,
what are the requirements?
For that we need documentation,
I'm gonna show you two forms of documentation,
the first is called an "exemplar".
An exemplar is a piece of valid C++,
it's a declaration that contains
all of the elements necessary for the concept.
However, it omits the definition.
Here's the body, exemplar, the name is body,
to inform the user of the concept,
it's got the nested value type,
which is a forward declaration,
it's got the declaration for
the write static member function,
and it's got some comments that tell you what to do.
Now why is this useful, well,
the user can take this and they can copy it
and they can paste it, and they can rename a few things
and add their code and then they can go.
Now in this example, it's small,
maybe that doesn't mean so much
but if you have a concept with a lot of requirements,
20 or 30 member functions,
you're saving them a little bit of time.
Also, people understand code.
Hopefully, programmers understand C++ when they see it.
So, a piece of code is gonna speak to programmers
more easily than exposition.
The next form of documentation,
which is more formal and more prevalent,
and what you're probably used to seeing,
is called the valid expression list.
The Valid Expression List shows you all
of the valid C++ expressions that are associated
with a concept as well as their semantics.
Here we got our symbols, "B" represents some type
that we wanna, we wanna confirm that it adheres
to the concept and then in our table,
we can see "B" has to have a nested type called value type,
in the description we explain the thing
that it's supposed to be or do,
B has to have a static write function
with a particular signature,
the description column is the place
where you put all of the information required
for the specification.
Your post-conditions, your pre-conditions,
the exception safety guarantees,
if you have any algorithmic complexity requirements,
those would go there,
and of course the explanation of what to do.
Now, this documentation is great for users,
but it doesn't help the compiler.
You can't take this and feed it into the compiler,
and then have it help you determine
if your types meet the requirements.
So we need to do something to allow the compiler
to be informed of a type that is valid for our needs,
and to do that, we're gonna have to dive
into some metaprogramming.
So, I'm gonna show you some metaprogramming,
the techniques that I'm gonna show you
are not state-of-the-art, they don't represent
all of the latest and greatest,
they're just some meat and potatoes C++ 11,
my goal in showing you this is not
to make you walk outta here as metaprogramming experts,
but just, you have an idea of how this sort of thing
is done, having an understanding of the basics
will help you in some situations.
For example, if you get a compiler error,
and you know the fundamentals,
then maybe you have a better chance
of figuring out why it's not working.
So, we would like to write a metafunction called "is body"
that determines whether or not a given type
meets the requirement of our concept.
Now, you've probably used these things
before in the standard library,
you've got "is reference", you've got "is constructable",
that sort of thing, so we're gonna write something like that
now why would a metafunction like this be useful?
Well, if a user passes a body type
that doesn't meet the requirements
and they try to compile,
they might get a really big error,
they might see a message that has nothing to do
with the real problem,
so one of the uses is to create some diagnostics
for the user.
If the user tries to call this function,
and their body doesn't meet the requirements,
they're gonna get a nice message,
it's gonna say, "Body requirements not met."
So, this is a mark of professionalism
for your library, users are gonna thank you,
people like to see this sort of thing, it's extra polish.
Another purpose for metafunctions
is to use with "enable if",
if you want to constrain a function,
so it only appears in the set of candidate overloads,
when the type meets the requirements you can do that.
So this is another tool in your metaprogramming toolbox.
So we're gonna write it.
Now, in order to write it, there is one thing that we need,
which is so incredibly useful, that we just have to have it.
And that is void underscore T.
Void underscore T maps a series of
zero or more types to void.
You're probably thinking, what use is that?
But trust me, it's gonna come in handy.
If you don't have access to C++ 17,
you can just copy the implementation, it's that simple,
it's just those two lines,
if you're using a slightly older compiler,
you might need to add another two lines,
you can find those on the internet
to work around a little defect,
most of you probably won't run into that.
So, we wanna create our "is body" metafunctions,
so we declare the primary template.
Here's B, which is the type we wanna test,
and then we have a second template parameter,
which is defaulted to void,
and we're gonna use that in our specialization.
The primary template is derived from
false type, which means that by default,
any B is gonna immediately be considered not valid.
Our strategy now is to specialize this class,
for the case where B is valid.
And the way that we do that is we introduce the void T.
Inside the brackets for the void T,
we're gonna put a comma-separated list of expressions,
which must be syntactically correct.
Now, you have to understand what "SFINAE" is,
substitution failure is not an error.
That means if the compiler substitutes a B,
which would result in a compile error,
inside the brackets of that void T,
rather than aborting compilation,
it will simply discard the specialization
from the set of candidates,
and it will fall back to the primary template.
That's the behavior that we want.
So, first we wanna check for value type,
all we do is we name the type.
So, you can see type name, B value type,
we put that in our list, so now we're checking
for value type, that's gonna be great.
So we're halfway done.
The next thing that we need to do
is check for a static write function.
Our strategy is going to be to call the function.
Now, we can't actually call the function, right?
This is a context where we're expecting to type.
So we're gonna use the decltype key word
to tell the compiler to give us the type,
as if the function was called.
So here we introduce decltype,
we call B write static member,
and then we need some variables
to pass in the parameter list, otherwise it won't be valid.
Here we're gonna use "declval," a C++ 11 feature,
inside the template parameter,
we've named the type that it has to be
in order for the signature to be correct.
And that'll give us a type that's gonna be right.
So, if B has that nested write function,
than "is body" is going to derive from true type,
otherwise it's gonna derive from false type.
Don't ask me about the void zero,
it just has to be there, I'm not really sure why.
I'm guilty of copying and pasting code from the internet,
to get things working, don't look at me like that!
I know you've done it too!
(crowd laughs)
So, you can find this code in my Github,
you can see that it compiles.
So what have we done?
Let's take a moment here and figure out what we've done.
We've created a generic algorithm.
Our serialization function is a generic algorithm.
The parameter that it accepts
meets the requirements of a concept.
A concept has to be well documented,
because the compiler's not gonna tell you
what it's supposed to do.
So, we've got the documentation
and we have our metafunctions to inform the compiler
if a type is gonna meet the requirements.
So, here's that serialization function that we wrote.
Now, if you remember, early on in the talk,
I talked about the algorithms that
we wanna perform on the container.
We wanted to serialize a message
but we also wanna parse it, so,
can we create a signature for a parsing function
that uses a standard input stream,
and writes the result into the message?
Turns out, we can!
So, here's an implementation,
it looks very similar to write,
the first thing that we do is that we read the header,
and then we delegate the responsibility
for reading the body to the body type.
So, now we need to add, we have our string body class here,
we're adding the static read function,
the implementation is very simple,
we extract the string from the input stream,
but wait a minute, we've just added to the requirements.
So, we need to document that.
So, we go back to our valid expressions list,
and I just add a row, here's the row for read,
you can see in the description it tells you
what it is that it's supposed to do,
but now that we've added to the valid expressions,
we need to inform the compiler,
so we go back to our metafunction,
and then we just add another item
in the comma-separated lists.
So, you can see that this is extensible
as you add more requirements,
you just put them into the tray,
or you put them into the rows
for your valid expressions table,
you just need to be diligent and orderly
about making sure that your concept is well specified.
Now, let's revisit the problem of allocator awareness.
So, do we have the technology to solve this problem?
Well it turns out,
we actually have already solved the problem for the body.
If we have a vector body,
we can just add an allocator parameter to that type,
and now, when the user declares a vector body,
they can specify the type of allocator.
However, we have a little problem.
Let's say the user has an allocator called
"my alloc" which is stateful.
In other words, it requires parameters upon construction,
it cannot be default constructed.
Our message class doesn't have any constructors,
so there's really no way for us,
with the declarations that we have so far,
to initialize that object.
That's no problem.
We just add a forwarding constructor,
we use perfect forwarding,
any parameters passed through the constructor of the message
will just forwarded to the body container.
Seems like a reasonable thing to do.
Now, body is now allocator aware,
let's look at the fields.
So the problem with the fields is
that we're stuck with a map and
we don't have any control over the allocator.
Is there something we can do to change
this declaration so that we can get that level
of customizability that we want?
The solution that I chose in my library,
is to simply add another template parameter
called "fields", and then derive the message,
from that type, seems a little weird
but you'll see why this works out.
So, we've created that derivation.
Let's revisit our primary template.
Here's a message class, now I've added the fields type to it
here's our serialization function,
I've added the fields template parameter,
our serialization function can now accept any message
that has any type of body, and any type of fields.
Now, the user has to know what to put there.
We've gotten rid of the map, we need to replace
that missing functionality.
We need an implementation of fields that does
the things that we need to do,
such as insert values and retrieve values.
So what we'll do is we'll create a new user-defined type,
called basic fields, that's templated on the allocator.
Solving our allocator problem.
And here's a couple of member functions,
this is just a minimal implementation,
you can imagine you can add more to this interface,
to give the user some convenience,
to save them some typing, I create an alias
that uses the default allocator,
and now we modify our request and response aliases
to use that default.
So now request and response are backwards compatible,
if you don't name the fields,
you get the default fields with the default allocator,
you can specify your own fields type if you want,
including a basic fields that uses your allocator, very nice
The constructor you can imagine, we just add to
our forwarding constructor
and we pass some parameters through the fields as well.
I won't get into those details.
Now, what does that look like?
So, if you have a request, using a string body,
now we can set a field, we're setting the user agent
fields to the value "chrome."
If we want to retrieve a field,
here you can see we're applying
the array index operator to the request object.
Notice how it looks like set and array index
are members of the message.
The reason that it looks that way,
is because we've derived message from fields,
so this is like a neat way to extend the interface
of a class, this is something that's a little bit,
it's a little off the beaten path,
deriving from a user-defined type
in a template parameter list,
so, at this point you're probably thinking,
wait a minute, we just added another concept,
we just added fields, what the heck is that?
So we need some documentation.
So we're gonna create a valid expression list,
only this time, you'll notice that
those member functions, they're not static members anymore,
these are members of an instance,
so in our notation, we have to have an instance
in order to form a valid expression.
So I've added some terminology, F and C are instances
of some candidate fields type,
we have the parameters N and V,
which are the strings that you need
to pass into that function,
so that's our valid expression.
Now, here's where we're at.
We have the allocator awareness for the body,
the fields can be customized,
the user can choose the implementation,
they can choose the allocator,
now we're done to one hard-coded allocator,
we have one standard string,
that uses the default system allocator,
how can we change this declaration,
so that even that string now is customizable,
without adding an allocator template parameter, of course?
So, the method that I chose is first,
we're going to refactor this class
to eliminate the data member,
I've converted over to string view,
now there are some people who think that
string view is harmful,
that could be the subject of another talk,
so I've added accessor functions for the reason string,
how are we going to implement them?
Here's an implementation for those functions,
we're calling these new functions,
called "get reason" and "set reason",
which are members of the class.
But these are not part of the message class,
so in order for this to work,
they have to be part of the fields.
So in other words, we're delegating the responsibility
for managing that string
to the fields container, whatever that is.
Now why would we do that?
Well the fields container is already good
at managing strings.
It manages the name value pairs,
so it's not unreasonable to give it
a little bit of extra responsibility,
for managing this one string, for responses,
and the two other strings for requests,
not a bad choice, so here's our basic fields,
you can see I've added the two accessors,
we've got the get and the set functions,
and I've marked them as protected.
Now the reason I did that is because message is
derived from fields so now the user can't
see those two functions,
they can't go behind your back
and mess with the implementation functions.
That's only possible because we used derivation.
But wait a minute, what did we do?
We added more requirements to fields!
We've gotta go back to our valid expression list
and add a couple of rows,
so the user knows what they need to put there.
So here's our get reason and our set reason
for the fields, you can imagine we're gonna need to write
a type trait for the "is fields",
we're gonna need to have all that in there.
So now, we're at a really good point.
We've solved a lot of problems,
we've done a lot of things
and I wanna say that you're the boss.
Now what does this mean?
We're all used to interacting with concepts
through the standard library.
Standard library's got lots of concepts,
they're really cool, it's got generic algorithms,
it's got flexible containers;
however, the standard library is designed
for general purpose computing, right?
It's general purpose, it's not specific to any domain.
But programmers like me and you, in the real world,
who have to solve problems,
they need to create the class employee record,
or they need to create the class invoice identifier,
they don't have the luxury of
designing vocabulary types, like vector.
So when you're gonna create these concepts,
you're gonna have to encapsulate a lot of little,
really annoying business rules
that don't fit neatly into things like "move constructable."
And, it helps to be able to have techniques
that will let you do that.
Now, you'll notice the techniques that I used
are not found in the standard library.
I have derived the class from a user-defined type.
I've used static member functions of a user-defined type.
And the reason that I did that is,
I wanna show you that you can think outside the box,
you can use any of the language features,
so that you can create a custom solution
that works great for your domain-specific use case.
So, at this point, I'd like to summarize the talk,
here are most of the declarations that we talked about,
if you have any questions and you wanna reference them,
that's very nice.
All of the code that I've shown you, it compiles,
don't worry I didn't put up code that's not gonna work,
I checked it before I came here,
you can access the repository and you can see what's in it,
you can access the slides, they're in a PDF file,
so you can look at that,
there's also a wonderful page on the Boost website
that discusses generic programming,
it overlaps a little bit with some of the things
I've talked about,
it also has some other interesting ideas
that you may find helpful if you wanna continue
your journey on education into concepts.
Now, if you wanna talk to me and ask me any questions
on any subject at all,
I have a very light schedule, I'll be here now,
and I'll be around the conference for the rest of the week.
And I'll also be appearing tomorrow night
at the "Meet The Speakers" Dinner,
which is a paid and scheduled event,
so if you would like to sit down the speakers,
not just me, but all the speakers,
you can do that, you can register for it
and we're gonna be there tomorrow night.
So, in closing, I would like to say
I'm very grateful to the people
that have put this conference together, John Cobb especially
they've given me the opportunity to bring my ideas to you
and share them with you,
can we give them a round of applause?
(applause)
Thank you!
Okay, the dreaded questions.
(laughter)
- [Audience Member] Great talk by the way.
Thank you very much.
- [Audience Member] My question is about the
static assert you put in,
to make the error message nicer
if the body doesn't match the concept?
Using the type trait that you defined?
Do you, is that actually what you use, in beast?
And does it help users give them
a message that's so broad,
just saying their type doesn't meet the requirements?
Is it, because if it didn't have that,
the compiler would give them
a noisy, but very specific message
about what it is that...?
Okay, so the question is,
do I actually use the C++ trait mechanism
that you saw in the slides,
in the actual library beast,
actually yes, that's what I use
and I keep it at C++ 11,
because there's still a lot of people using C++ 11,
it is actually helpful when users stick to the types
that come with beast,
of course they're not going to have any problem,
but as soon as they create their own type,
they're gonna get that message,
and the documentation in beast is very explicit
about what the requirements are.
So they can go back to that documentation and check it.
- [Audience Member] So, you're finding that users
find your documentation more helpful
than the compilers that boast error messages?
Absolutely, yes, the feedback has been overwhelming
that having the additional diagnostics is useful.
That's one of the things that came out
during the Boost review.
- [Audience Member] Great, thank you.
Thanks.
- [Audience Member] Yeah, I got a question regarding
your use of the body and the fields structures
where you said that you pass the additional
arguments to construct it to the buddy constructor,
Right.
- [Audience Member] Now, you said you can do the same
with the fields constructor,
how do you construct, how do you know
which of the arguments that are passed,
go to the buddy constructor
and which go to the fields constructor?
Okay so the question is, when we added the fields,
as a customization point,
how do we now have constructors which can route
zero or more arguments to the body,
and zero or more arguments to the fields,
is that the question?
- [Audience Member] Yes.
Okay, so, I could've put that on a slide,
but it would've been a really big slide,
the solution that I chose is just
to just copy the interface of pair, right?
Because pair solves that problem.
Pair has a whole bunch of constructors,
it's got a single argument constructor,
it's got two argument constructor
for initializing each of the members,
and then it has something called
the piecewise construct, so if you piecewise construct tag,
then the second and third parameters are tuples,
and that tuple can be forwarded,
now of course, that would be too much,
too much for the slides.
You can see that in beast by the way,
beast has the, if you wanna see the body concept
and the message container in beast,
it's much more full-featured,
it solves all these problems,
you can take a look at it,
you can look at the documentation
and see how the concepts are actually specified,
there's more to it,
what I've done for this talk is
I've dramatically simplified it,
so that the slides will fit,
and to not overwhelm the audience.