flak rss random

three valued structs

Sometimes we have a boolean, which is great for storing two values, but we need just a little more space to squeeze in a third value. There’s a few ways to do this.

int

A common technique is to use integers. 0 for false, 1 for true, -1 for the mysterious other.

For example, we might consider how many burritos we want to order. Anything under 100 sounds like a terrific lunch, but more than that seems excessive. A negative number of burritos is simply nonsensical, however.

int
simplecheck(int x)
{
        if (x < 0)
                return -1;
        if (x < 100)
                return 1;
        return 0;
}

void
testfunc(int input)
{
        if (!simplecheck(input))
                panic("bad input");
}

Alas, as the above shows, this is susceptible to a common error, checking only for false and treating both unknown and true as true.

The simplest way to correct for this is to assign 0 as the good case. If somebody accidentally treats our three valued big boolean as a real boolean, we want the partitioning to maintain the separation between good and bad values.

structs

Borrowing a trick from strict structs we can use some struct literals for our values. Unfortunately, C doesn’t allow for directly testing struct equality, so we use pointers.

struct threebee {
        int v;
};      

struct threebee *isokay = &(struct threebee){ 0 };
struct threebee *notokay = &(struct threebee){ 1 };
struct threebee *noidea = &(struct threebee){ -1 };

struct threebee *
check(int x)
{
        if (x < 0)
                return noidea;
        if (x < 100)
                return isokay;
        return notokay;
}       

void
func(int input)
{       
        if (check(input) != isokay)
                panic("bad bee");
}       

Why use structs? To avoid mixing return codes from multiple check functions.

typedef int intcode;

intcode *intokay = &(intcode){0};
intcode *intbad = &(intcode){1};
intcode *intmissing = &(intcode){-1};

typedef int floatcode;
floatcode *floatokay = &(floatcode){0};
floatcode *floatbad = &(floatcode){1};
floatcode *floatrange = &(floatcode){2};
floatcode *floatprecision = &(floatcode){3};
floatcode *floatmissing = &(floatcode){-1};

int *
intcheck(int x) {
        if (x < 0)
                return intmissing;
        if (x < 100)
                return intokay;
        return intbad;
}

void
badfunc(int input)
{
        if (intcheck(input) == floatokay)
                panic("no no no");
}

Here we have accidentally tried to compare the validation of the number of burritos to order with result codes for the price check, which we have ever so wisely chosen to represent with floats.

Anyway, all of this fails if we miss the check entirely.

void
missfunc(int input)
{
        if (!intcheck(input))
                panic("bad input");
}

Our check function never returns null, but alas this still compiles.

strict

More strictly, we can use structs without pointers.

struct threebee goodbee = { 0 };
struct threebee badbee = { 1 };
struct threebee nobee = { -1 };

struct threebee
beecheck(int x)
{       
        if (x < 0)
                return nobee;
        if (x < 100)
                return badbee;
        return goodbee;
}       

void
nocompile(int input)
{       
        if (!beecheck(input))
                panic("fail");
        if (beecheck(input) != goodbee)
                panic("fail");
}

The compiler gods will not approve of this usage, however. On the one hand saving us from an error, but also failing to play nice when we’d like that.

We need a macro.

#define isgood(b) ((b).v == 0)
void
beefunc(int input)
{       
        if (!isgood(beecheck(input)))
                panic("no good");
}       

This seems excessive, but wait, it gets worse. You just know somebody is going to write this macro next.

#define isbad(b) ((b).v == badbee.v)
void
beefunc(int input)
{       
        if (isbad(beecheck(input)))
                panic("is bad");
}       

And now we have the same problem we started with, checking for only a single failure condition. All this fuss, for no gain.

zero

So that was a torturous loop to cycle through. The compiler offers us limited help here.

But we can make the simple change to using 0 to indicate success.

int
hasproblems(int x)
{
        if (x < 0)
                return -1;
        if (x < 100)
                return 0;
        return 1;
}

void
shouldbuy(int input)
{
        if (hasproblems(input))
                panic("no buy");
}

A more precise return code is available if we need it, but we have nicely partitioned success and failure onto opposite sides of the boolean divide. Quick checks behave in the obvious manner.

If consistency dictates that the return code absolutely must be true for success, another parameter can be added to pass back explanations. This is less prone to confusion.

bool
areyousure(int x, struct threebee *whynot)
{
        if (x < 0) {
                *whynot = nobee;
                return false;
        }
        if (x < 100)
                return true;
        *whynot = badbee;
        return false;
}

This is probably more involved than necessary, in my opinion. Just remembering that 0 may make for a better success indicator should suffice when designing an API. Too late for the ones already loose in the world, though.

Posted 29 Jul 2020 06:20 by tedu Updated: 06 Aug 2020 01:17
Tagged: c programming