Search code examples
coopenumssoftware-design

C, proper use of enums, unions and function return


My goal is to achieve best code / design practices:

I have functions that return 2 possible enums which can be a (state enum) or an (error enum).

in order to distinguish between the two enums, they have to be far values from each other and never overlap for example:

other modules has function are expecting the (error enum). and i do not want to make mapping function from state to error. so i have to contain (enum error) inside (enum state).

Enum eSomeState{
    enum1Value = 0,
    ...
    enum1Value99 = 99, // <<max
};

//defined in different module:
Enum eSomeError{
    enum2Value   = 100, // <<min
    ...
    enum2Value99 = 199,
};

as if they overlapped, it will be impossible to know which one was returned from the called function (without extra flag).

So, earlier in my code i used to define two enums, (combining all error in error enum) into (state enum) .. like following

Version 1:

/* used by this module */
enum eSomeState{
    eSomeState_A    = 1,
    eSomeState_B    = 2,
    eSomeState_C    = 3,
    eSomeState_D    = 4,
    /* do not reach 100 */

   /* DO NOT GO BELOW 100 */ <------------- copied from other module's enum
   /* every time i update eSomeErrorFromOtherModule, i have to come here and update this as well */
   eSomeStateError_ErrA    = 100,
   eSomeStateError_ErrB    = 101,
   eSomeStateError_ErrC    = 102,
};

/* used by this module and other module that cares about error handling */
/* defined in different scope */
enum eSomeErrorFromOtherModule{
   eSomeError_none    = 0,
   /* DO NOT GO BELOW 100 */
   eSomeError_ErrA    = 100,
   eSomeError_ErrB    = 101,
   eSomeError_ErrC    = 102,
};


bool doSomthingA( enum eSomeState* const state){
    Assert(*state == eSomeState_A);

    //do stuff

    if (success){
        *state = eSomeState_B;
        return true;
    }else{
        *state = err;
    }
    return false;
}

functions from other modules are expecting (Error Enum) type so i used to cast the (State Enum) to (Error Enum)

after some time, i noticed that its hard to keep copying the (Error Enum) to (State Enum) and keep them identical everytime i need to modify one of them.

so i moved to Version 2:

    /* used by this module */
    enum eSomeState{
        eSomeState_A    = 1,
        eSomeState_B    = 2,
        eSomeState_C    = 3,
        eSomeState_D    = 4,
        /* do not reach 100 */
    }

    union uSomeState{
        enum eSomeState state;
        enum eSomeErrorFromOtherModule err;
    }

    /* functions updated to */
    bool doSomthingA( union eSomeState* const state){
        Assert(state->state == eSomeState_A);

        //do stuff

        if (success){
            state->state = eSomeState_B;
            return true;
        }else{
            state->err = err;
        }
        return false;
    }

version 2 [i didnt test it as i barely used union but i expect it to work] have reduced my work to go copy enum from another module to this module .. but i still have to make sure that (state enum) doesnt overlap (error enum) ..

  • whats your thoughts about the code above
  • what do you suggest to improve more ?

Solution

  • I like to think of enums as a set (list) of constants. If i have to store a state, then i introduce a new type, e.g. bitset_t or state_t.

    Example:

    typedef int state_t;
    
    enum someStates {};
    enum someErrorStates {};
    
    bool doSomething(state_t state) {}
    

    Because sometimes it is necessary to 'combine' two states together (e.g. by the operator '|'), which is not in the set of either enums.

    Just an opinion.

    Supplement:

    Basically, an enum is an int (unsigned?) and you can store upto INT_MAX values in one set. But if you want a 'bitset', then you are restricted to 32 (if int are 32 bits) distinct values. If you want to have more, then you have to implement a bitset, an array of words.

    Example:

    #define word_size sizeof(unsigned int)
    #define word_width (word_size * CHAR_BIT) // defined in limits.h
    #define word_index(Pos) ((Pos) / word_width)
    #define bit_index(Pos) ((Pos) % word_width)
    
    unsigned int bitset[N];
    
    bool isSet(unsigned int bs[N], unsigned int pos)
    {
        return ((bs[word_index(pos)] >> bit_index(pos)) & 1);
    }
    
    void setBit(unsigned int bs[N], unsigned int pos)
    {
        bs[word_index(pos)] |= (1 << bit_index(pos));
    }
    
    void clearBit(unsigned int bs[N], unsigned int pos)
    {
        bs[word_index(pos)] &= ~(1 << bit_index(pos));
    }
    
    void toggleBit(unsigned int bs[N], unsigned int pos)
    {
        bs[word_index(pos)] ^= (1 << bit_index(pos));
    }
    

    And then you could define your enums like that:

    enum someState {
        state1 = 1,
        state2 = 2
        //...
    };
    
    setBit(bitset, state2);
    isSet(bitset, state2);
    // ... and so on
    

    Basically, the enum constants will describe the (bit) position in the bitset.