Affinity to signednedss
- 7 minutes read - 1330 wordsIt continues to amaze me when I look at other peoples code, how many good developers think in signed integers. This is a short example of such code and a walk-through of why it is mis-guided.
Signedness in the world of computers Almost nothing in the world of computers is signed; you cannot send a negative number of bytes, you cannot have a negative amount of memory, you cannot put a negative number of elements in a vector and so on and so forth. We will frequently subtract numbers, but we will usually never have negative results.
For example: I may want to send a buffer using a send routine that does not necessarily send all data at once - subtacting the sent number of bytes from the full number of bytes can never be negative (in other words, the routine can never send more than it was given).
Good code like the STL realizes this and typically uses the
size_t
type for sizes in general (basically it is
used for everything you can count that you can keep in memory). It is
of course unsigned (for the aforementioned reasons) and it is as large
as it needs to be, for any count of “things you can have in memory”.
The problem then arises when a signed-integer-loving developer mixes his int-using code with the better size_t-using code. Good compilers will complain about pitfalls in signed/unsigned arithmetic and the developer is forced to respond with an equal measure of type conversions or casts.
The result will usually be unnecessarily verbose code that is less capable of handling large amounts of data (since the sign bit can no longer be used for actually counting). Granted, the last bit is more a problem on 32-bit systems than it is on 64-bit systems, at least at the time of this writing - but you never know then someone decides to use your code on a 32-bit system. If your code is any good (and often even if it isn’t), it will end up on all sorts of systems without your knowledge anyway…
Let’s take a look at an example I came across today: This is an extract of a piece of production code that actually works, it’s just unnecessarily ugly because the developer had an unhealthy affinity towards signed integers:
int sent_bytes = 0;
while (sent_bytes != static_cast<int>(data.size()))
{
int res = SSL_write(m_ssl, data.data() + sent_bytes,
int(data.size()) - sent_bytes);
int err = SSL_get_error(m_ssl, res);
switch (err)
{
case SSL_ERROR_NONE:
sent_bytes += res;
notifyObservers(event_t::conn_tx, res);
break;So we have a variable
sent_bytes
with a nice readable
descriptive name, so far so good. It escapes me how that can be a
signed integer - how exactly do we send a negative number of bytes?
Right, that is difficult indeed. By making it
int
rather than
size_t
, we have either 63 bits instead of
64 bits for the counter (which is probably fine) or 31 bits instead of
32 (which may be a problem if we use large buffers). But regardless of
whether the more limited positive range is a problem or not, we don’t
get anything in return. Nothing is gained by opening up for
negative numbers; there is no plausible scenario in which we could
ever have sent a negative number of bytes. So we get nothing for
something and that’s never a good trade. Now I would buy the argument
for making this a signed integer if it somehow made the rest of the
code more readable - let’s proceed with the analysis then.
So in our loop we compare the number of sent bytes to the actual
number of bytes we wish to send. Notice how a
static_cast<int>
is used to stop the compiler from
complaining that it is dangerous to compare the two types (as they
don’t have the same range a conversion is made - and that is not
necessarily what you want). Now consider for a moment what this cast
accomplishes… It casts the value so that the compiler stops
complaining - it does not actually solve the problem the compiler is
complaining about. This is like shutting off the fire alarm instead of
putting out the fire - it stops the noise but it doesn’t really solve
the problem. The problem of course would (as of this writing) most
likely only occur on 32 bit systems where we would have a 2.5G buffer
and after sending the first 2G our sent_bytes would wrap and depending
on the rest of the logic we may have more or less luck with our
algorithm when sent_bytes is -1.5G (or some other number - depending
on the good will of the compiler; read on)
And this is actually what makes it even more difficult for me to understand: Unsigned arithmetic in C++ is valid and well defined - it is integer arithmetic modulo 2^n where n is the bit-size of your integer. In other words, when you add one to your maximum integer value, you’re back to zero. There’s nothing magical about that, but it is extremely useful. In contrast, the standard does not define behaviour of signed overflow - if you rely on signed integers and they overflow, the behaviour is undefined by the standard. Why would anyone tend towards a type that has less standardized behaviour if it does not provide a benefit? The mind boggles.
We move on to the
SSL_write
line. Here we must
subtract the number of sent bytes from the total size of our buffer so
as to compensate for adding the number of sent bytes to the start
offset of our data buffer. The developer has chosen an alternative
means of getting around the compiler warning this time: Construct a
temporary signed integer from the unsigned size of the buffer, then
subtract the signed
sent_bytes
from there. Now, if we
were to attempt to send 2.5G of data on a 32-bit platform this code
would explode, but the compiler does not tell us about this because we
skillfully mislead it by inserting valid constructs that hide our real
crimes from the compiler.
Ultimately, we add our signed integer res to our number of sent
bytes. Naturally, we expect that res cannot be negative when we use it
in this manner - no assertion or otherwise is inserted for this, but
that is something one could have done. Or we can trust the library to
honour its contract and never return success and set
res
to a negative value. This concludes the use of
sent_bytes
, and in no situation do we gain anything
from having it be a signed value. How about we consider a fixed
version of the code:
size_t sent_bytes = 0;
while (sent_bytes != data.size())
{
int res = SSL_write(m_ssl, data.data() + sent_bytes,
data.size() - sent_bytes);
int err = SSL_get_error(m_ssl, res);
switch (err)
{
case SSL_ERROR_NONE:
sent_bytes += res;
notifyObservers(event_t::conn_tx, res);
break;Note the distinct lack of casts and explicit construction of temporaries to work around type mismatches. Other than that, the only functional difference between the two versions are, that this shorter and more readable version will successfully send 2.5G of data on a 32-bit platform, rather than blowing up unpredictably.
I fear that the affinity for signed integers comes from the old “magic
return value” where we use non-negative values for the real return
value and assign special meaning to negative magic numbers that
callers must remember to check for (like
SSL_write
in
the example earlier). That, of course, is the worst excuse; especially
when working in C++ that actually has means to deal with exceptional
circumstances where less fortunate C developers might feel compelled
to return negative magic numbers.
Old habits die hard I guess. But it troubles me that I see this from young developers - they did not code C back in the ’90s (in fact they were born in the ’90s). I don’t have all the answers - this one is definitely a mystery to me.