ssize_t is useless type.

POSIX introduced ssize_t. The only reason why that ssize_t is exist is to represent a -1 number easily. Obviously, there cannot be a “negative” size of an object, or a number of bytes, or whatever like that.

Common system calls such as read and write return ssize_t. Thus, they are required not to return a number greater than 0x7fffffff if size_t is 32 bit integer.

While this does not limit you from reading files, and it is nothing to deal with large file support (although it could be matter of time and someday programs will start to transfer 2GB packets easily), from an idealist point of view it is useless.

See: it is just like size_t divided into half, just to represent a -1 number naturally.

One however cannot just type something like that:

size_t x;

x = read(fd, srcblk, sizeof(srcblk));
if (x == -1) { error stuff }

The comparison with -1 on 3rd line will logically fail because x is unsigned (but see below).

For pedant paranoid programmers who mystically fear type inconsistency and that nasty magic signed-unsigned integer exploitations and integer overflows, comparing signed and unsigned types would be unnatural. I am kind of that person too.

To solve this, I rely on size_t, and introduce this macro instead:

#define NOSIZE ((size_t)-1)

, and do comparison this way:

if (x == NOSIZE) { error stuff }

The read() syscall can be left as is, but to shut up possible warnings, a cast could be used:

x = (size_t)read(fd, srcblk, sizeof(srcblk));
  • and this is especially important to follow if this is a part of a library function.

Of course, since that you must remember that you have now a magical NOSIZE macro, with which you now must compare all things that functions with ssize_t return type do return.

For me, size_t is also consistent type, and many platforms other than Linux usually define it, while omitting ssize_t, since size_t is required type for sizeof operator.

Beyond magic

This little trick is useful not only for filthy ssize_t type redefinition. If your type requires -1 value only, and otherwise should be of unsigned type, you can do the same. For example, my code requires a 64 bit unsigned type to represent a size of file. It would be too wasteful to define it as signed just to show -1 number as -1 anyway and leave all other 9 223 372 036 854 775 807 values out. So I do:

typedef unsigned long long my_fsize;
#define NOMYFSIZE ((my_fsize)-1)

, and then all my_fsize functions and variables are able to store anything but NOMYFSIZE, which is the only reserved value for error or “no value” situations.

Well, novice C users can ask how it works? Everyone who is used to C already knows that it’s a hacky language. Even languages that are designed to be safe, but descend from C also contain some of the C tricks, for example, bit manipulation with raw logic gates and normal integer overflows (now omit memory allocation, which is usually an OS/runtime preference of how it should be organised).

Here, the -1 casted to size_t internally (invisibly to programmer) turned into 0xffffffff, the maximum value for a 32 bit unsigned integer. It is usually a ~0 constant too (which could be abused for your dirty obfuscation tricks), but diving into history could reveal you facts that it was not always like that on many other very old and now almost extinct machines. Because -1 is turned into 0xffffffff (again, I discuss about 32 bit registers), the further comparison does work.

So, is ssize_t is useful anyway?

This test program:

#include <stdio.h>

int main(void)
{
	size_t t = ((size_t)-1);
	printf("%d,%d,%d\n", (~0 == -1), ((unsigned)~0 == -1), (t == -1));
	return 0;
}

gives “1,1,1” if being run on modern system. More, both gcc 5.3.0 and clang 3.8.0 run quiet with “-ansi -pedantic -Wall -std=c89” options. While first comparison is pretty legal, second may be left on a compiler decision how to act and warn, third is illogical - or at least a warning should be there.

OK, giving “-Wextra” finally reveals the two warnings: the second and third, as it should be:

rys@rys:~$ LC_ALL=C gcc -Wall -Wextra -ansi -pedantic -std=c89 t.c -o t  
t.c: In function 'main':
t.c:6:49: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
  printf("%d,%d,%d\n", (~0 == -1), ((unsigned)~0 == -1), (t == -1));
                                                 ^
t.c:6:60: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
  printf("%d,%d,%d\n", (~0 == -1), ((unsigned)~0 == -1), (t == -1));

And clang looses the second one, objecting only to the obvious point:

rys@rys:~$ LC_ALL=C clang -Wall -Wextra -ansi -pedantic -std=c89 t.c -o t
t.c:6:60: warning: comparison of integers of different signs: 'size_t' (aka 'unsigned int') and
      'int' [-Wsign-compare]
        printf("%d,%d,%d\n", (~0 == -1), ((unsigned)~0 == -1), (t == -1));
                                                                ~ ^  ~~
1 warning generated.

, but reveals the real type of the -1 thingy: plain int (as all the compilers defined numerical and enumerated constants anyway).

With all that stuff implicitly working anyway, there is the question: is ssize_t really useful as of today?

With of without described trick it is still gets treated equally and treated right. The trick is only required to keep code clean and safe to port to other compilers/systems etc, and to get rid of potential warnings anyway.

ssize_t probably was created in that dark ages when compilers were not such smart and intelligent, but only tools to ease translation from human to machine language. Many warnings simply annoyed programmers, and type unsafety was a concern. As of today, it is compiler task to guess the human intent and generate code according to it, and finally, dark ages are in past.

I do not think ssize_t is worth using. It became a source of more potential bugs between size_t and ssize_t misunderstandings. While compiler gets things right, some old ones may still not. Beware.

Written on February 9, 2019