Thomas Rodgers10:11
Now this handles endianness not particularly well. We're going to become more selective about this as we go. So right now, each time that this gets called, it's going to present a different T, right? A reference to a different T, and it'll be called one for each element that's visited by for each. It'll be called once. And so that's the writer.
So the first thing that we typically will want to do is handle those types. There is a proposal to add a generic or somewhat generic ntoh and hton, but it's only for unsigned integral types. So if you happen to receive signed integral types over the wire, the proposal as it stands today is not going to help you. But it's pretty easy to write your own generic versions of these that do the right thing for the integral types that you're going to expect to get. Having done so, basically for each type, for each T, you can say net to host, and then you do the buffer cast to a T star, dereference that for a given buffer, and assign into your value.
Then you assign... call where to start reading from by doing this. The writer case is basically the same thing, except we're assigning into the buffer instead and advancing the buffer.
So it's often that you will have fields that represent enumerated values. Message type is kind of a prime candidate in this structure. C++11 added scoped enumerations, which has this convenient ability to specify the actual underlying type that you want to use, so you don't get whatever the default type for enum is. You can be explicit about it.
Then we need to make this more selective. And that's where this auto arrow syntax... is this familiar to everybody? Everybody pretty familiar now? Okay, not you. Okay. So the idea is that with auto, this is not auto as a C++14 return type deduction; it's basically a placeholder to say I'm going to tell you the return type later, and the arrow is pointing to or essentially the type that's going to be yielded from this function.
Next thing is, who all is familiar with enable_if and how that works? The whole thing gets removed from the set of considered overloads; it won't be compiled, and that's okay in C++ because there's this SFINAE, or substitution failure is not an error property with templates. But if it is a valid overload, the return type will be void, just like it was before. This is just a way to push this goo for selecting the type down so it's not part of the interface contract for this function. You could have just as easily put the typename enable_if before the operator call, but then what you're actually calling and what it expects is kind of lost in the noise of the enable_if. I'm told by Andrew Sutton that...
So at this point, this will now only select for integral constants. We won't accidentally be able to pass an enum to it. And then we can just add an overload that knows how to handle any type of enumeration using the same approach. In that case, we will ask the enum for its underlying type. So this will get back the uint or uint32_t for us as type V. As the type of V, we can just delegate back into the reader at that point because we already know how to read that type. And then we can static cast it back to the enumeration type, and we're done with the enumerations.
So a lot of protocols will have fixed tag data. These are things like magic numbers. Appropriately named magic inversion seems to be a good candidate for this. So we can declare those as an integral constant uint16_t, the type or the value that we expect, and uint16_t value we expect for magic inversion. And then you add the corresponding overload to your reader. One thing to note here: integral constant doesn't store any data, so that member of your struct will still have space associated with it; there'll be no value there, there's nothing that's being set into it. We grab out the underlying value type, which in this case will be uint16_t, we delegate back into the reader to read it, and then we can test to make sure it matches the expected value.
Time value that's baked into the integral constant T and write it to the wire.
So the more interesting part of all this is dealing with variable-length types: strings, arrays of integral types, maps, whatever. For the purposes of this talk, I'm going to assume that we represent strings as a uint16_t length and some variable number of chars that will be read immediately following the uint16_t. The implementation of this is pretty unsurprising: you just add an overload for std::string, read the length, construct a string for that many chars in the buffer, advance the buffer by the length of the string.
Length prefix, and you're going to read some number of elements of type T. We don't care about T because we've already started to build up a structure for generically reading T's from the buffer. So read the length, consume each T in order, and then place them in the back of the vector that we're building. Maps will work... oops, got a... magically better. I am eliding that check here. There's also other cases where you can, on the right side, if you pass in a vector bigger than is representable by the length...
Length of the map, you write out the key, you write out the value. Or is this reading? So you read in the key, read in the value, and then place that key and value into your unordered_map.
So at this point, you can pretty much do this kind of a structure. I've got a bit of fixed-length stuff at the beginning, a variable-length structure of variable-length header properties, a message type, and then some variable-length content at the end. Doing this with fixed overlays gets to be pretty tedious, just to cover this much, in my experience anyway.
So I've totally glossed over the issue of framing. At the end of the wire, they'll send you a fixed-length header that'll tell you how much data you're going to get back, and then you can read that fixed-length header. When you've got that many bytes, then you can read length number of bytes and then continue to process from there. If you have framing that's essentially encoded in reading the types... I've worked on a couple of examples implemented using Boost ASIO's stackless coroutines to do this. It's pretty onerous, but it can be made to work, but it's beyond the scope of what I want to talk about today. So we're just going to assume that the fixed header tells me everything I need to know to read the rest: the number of bytes to consume, everything else I need in a given message.
32 bytes of header off, and I want to return buffer minus 32 bytes to be consumed by a subsequent call. Something like this: you'd read your fixed header, you would read whatever the length number of bytes you got out of your fixed header were, and then you would call read to deserialize the remainder of the buffer for however many types that you had in here. Obviously at this point, you'd probably switch on message type and then make further decisions about how to decode the remainder of the message.
So user-defined types. One of the things we deal with in the financial world are prices, and people don't like their prices being rounded. What they use is a signed exponent and a 32-bit unsigned mantissa. So this is a case where the standard proposed ntoh and hton wouldn't work because you've got a signed 8-bit quantity, and you would not be able to pass that to the proposed standard hton and ntoh generics. The reasoning there is that somewhere in a museum somewhere, there might be a one's complement machine that might want to still exchange data with you over the wire. And so representing negative numbers becomes a problem if you just blindly cast them to the unsigned bit representation and back. But in practice, people do it all the time and nobody gets bit by it.
So for user-defined types to use this technique, it's pretty important that they have cheap default constructors. Otherwise, this all falls apart. So this is obviously a pretty cheap default constructor, and you could go ahead and read it this way. You add an overload for decimal T, read it for the exponent, read it for the mantissa, and then just construct your decimal and assign it through. But we're going to come back to that in a minute because that's not the most general way of doing it, because you're left basically writing those overloads for every single user-defined data type that you want to be able to instantiate.
One of the things I deal with a lot are option products. Not... will be an option over a given underlying. So in this case, this is a September 567.5 call, I believe on Google. The price is encoded there, and it expires on the 13th of September. And a put or call, indicating whether or not I have the right to buy at that price or the right to sell. There are other fields in options as well, a lot of those are exchange-defined, but this is sort of the meat of what you would use to represent one, and it would end up looking like this. So this is a variable-length structure because it has two strings representing contract and underlying ID in it, but it's otherwise not terribly interesting. And I haven't really covered price date reading, but...
They change them from time to time, but that's kind of beyond the scope of this talk. But another thing that we also deal with is, once you have options, you may want to create a standardized contract which lets me buy and sell different combinations of them and then give that a name. And that's what a listed spread is. It'll have an exchange-assigned symbol ID, and it'll have some variable-length collection of contracts associated with it. So it looks like this. But unfortunately, we can't actually read this. We know how to read the vector, we know how to read the contract ID, but what we don't really know how to do is read T when T itself is a Fusion struct.
Implementing that is pretty straightforward. We can test to see if T is a Fusion struct, and if so, we can recursively read it. The writer case is the same exact thing, except it's going to take a constant reference to the field to write.