One of the things that I passionately hate about software development is defining the same data in multiple places. The reason for this is that it massively increases the chances of getting an annoying, hard to solve bug for no good reason.
The easiest way to explain this is with error codes. For error codes, you need:
An enum type of some kind, so that you can pass the error around (unless, of course, you’re using something like an HRESULT, but that has its own thorny class of problems akin to 4CC-registration on ye olde Mac OS).
A unique numeric value.
A text string that can be used to show the user.
A text string for the enum value so that you’re not guessing which error you’re actually dealing with.
One way we could do this is to give enums (and other types) a string representation. (And we should probably do that anyway – we can generate that data on the fly when we first use it in code). But that only solves for the error-message case, and it doesn’t really help with the human readable text string lookup.
This article is going to be a lot simpler than the other articles on C!! (which hopefully are still percolating a bit). Mainly because I want to handle something simple before I dive into deeper waters – like generators.
So if you were going to redefine C++’s type system, how would you do it?
My goals are to make the types explicit sizes where possible, and obvious where not. And some of these are (as with everything else in C!! at this point) up for debate.
By the way, simple doesn’t mean short, sadly. This is a long one.
One of the more annoying things that you have to deal with when writing code that targets a variety of different platforms is endianness. You see, like in Gulliver’s Travels, there’s two camps in the computing world – those who break numbers up into clusters of 8-bits, starting at the least-significant part of the number and laying it out a byte at a time that way in memory (little endian) and those who start at the most significant 8-bits, and lay them out a byte at a time in memory until they get to the smallest part (big endian).
(Actually most chips these days seem to let you strap a pin to signal or ground to flip the default order, or set a bit in the BIOS to change it to whichever order you want, but most people use the default).
This is really annoying if you’re a game developer. PCs are little-endian. Old-school Macs? Xbox 360? Wii? PS3? Yeah, they’re big-endian. Network developers too; port numbers are always big-endian. And you need to remember that, or you’ll have problems.
This means that you end up littering your PC-based editor code (your development environment) with code to swap the byte order during serialization – which is harder to read than it should be. And if you forget, the bugs can be difficult to find. And if you take stock code written for one platform and move it to another, you often have to go in and change all kinds of things.
Let’s fix that.
Let’s leave this relatively heady mapping/contract/transformation stuff aside for now. I think we’ve got enough to chew on there for now.
To address one of Jonathan Blow’s requirements, let’s promote unique_ptr, shared_ptr and weak_ptr to first-class language constructs. (Partly because this way, the compiler should be able to step in, and do a better job than compiling down code to generate the same things).
We’ll do this by adding some new keywords that modify pointer declarations – owned, shared and weak – and in the process simplify the syntax for using these in the process.
A couple of friends pointed out that contracts really need to support true duck-typing to be useful in all circumstances. (Not everyone cares all the time about ultra-high performance code). Yet I still want to make sure that they’re high performance if you want to pass them around as types. So let’s see if we can find a happy medium.
A mapping in C!! is the definition of a transformation between two types or two contracts (and, by extension, define mappings between two types that honor the contracts in the mapping). Boy, that’s a mouthful. Oh, and I’m not done figuring out the syntax for this – so beware, it’s going to be very rough around the edges.
So let’s say you have Type A (or Contract A) and Type B (or Contract B).
You can define a mapping between them like this:
loose * mapping A ==> B
value = (float)in.a;
loose * mapping B ==> A // this is the reverse mapping
a = (int)in.value;