mupuf.org // we are octopimupuf.org

CVariant, a C-container That Can Handle Strings, Int and Floats

Would you fancy a C-con­tainer that can hold a string, an in­te­ger or a float ?

There are some cases where you need, for in­stance, to re­turn a value that could be of any type. This case just hap­pened to me when I needed to han­dle pa­ra­me­ters for PPass­Keeper mod­ules.

In this ar­ti­cle, I will ex­plain a so­lu­tion to solve this prob­lem from the ba­sics to the fi­nal code I did.

The ba­sic tech­nique for cre­at­ing multi-type con­tain­ers

The sim­plest so­lu­tion

To cre­ate a multi-type con­tainer, you need to store 2 things:

  • The type: The type of the data we want to store
  • The data: What we want to store

This can be done us­ing a sim­ple struct and an enum:

typedef enum {c_string, c_int, c_float } data_type_t ;
typedef struct
{
    data_type_t type;
    void* data;
}container;

To ini­tial­ize it, you would do :

container my_string {c_string, (void*)"a string"};
container my_int {c_int, (void*)42};

And to read the value, you would do :

container var{ ...., .... };
switch(var.type)
{
    case c_string:
        printf("Value is %s\n", (char*)var.value);
        break;
    case c_int:
        printf("Value is %i\n", (int)var.value);
        break;
    ...
};

This so­lu­tion works but we have to cast the value any time we want to set or read data from the con­tainer.

Bet­ter so­lu­tion : The use of unions

In this so­lu­tion, we will get rid of the cast we needed in the pre­vi­ous so­lu­tion. To do so, we will need the com­piler to do some checks for us at com­pi­la­tion time. Sounds good isn’t it ?

we will use (fol­low the link if you can’t re­mem­ber the use of union).

As we know, unions are a way to do poly­mor­phism in C/C++. The prob­lem is that it doesn’t store the type of the data stored. We will ad­dress this prob­lem us­ing the same pat­tern we used in the first so­lu­tion :)

So, let’s use an union now in­stead of a void* to store data:

typedef enum {c_string, c_int, c_float } data_type_t ;
typedef struct
{
    data_type_t type;
    union{char* str_v, int int_v, float float_v };
}container;

To ini­tial­ize it, you would do :

container my_string;
my_string.type=c_string;
my_string.int_v="a string";

container my_int;
my_int.type=c_int;
my_int.str_v=42;

And to read the value, you would do :

container var{ ...., .... };
switch(var.type)
{
    case c_string:
        printf("Value is %s\n", var.str_v);
        break;
    case c_int:
        printf("Value is %i\n", var.int_v);
        break;
    ...
};

OK, we got rid of the casts but the ini­tial­iza­tion takes 3 times more lines !! That’s true, but it is not re­ally a prob­lem as we are about to do an Ab­stract Type from it ;)

Cre­ate an Ab­stract Type

Now we have a proper so­lu­tion to store type-vari­ant data, we need to cre­ate an Ab­stract Type for it. This means we will cre­ate func­tion to con­struct the con­tainer, to ac­cess it and to free it.

The use of Ab­stract Types are im­por­tant be­cause it al­lows you to change the in­ter­nal struc­ture of your con­tainer with­out touch­ing any­thing else. This also helps to get a much more un­der­stand­able code for peo­ple main­tain­ing your pro­gram as they won’t need to read any header, your code would al­ready be self ex­plana­tory.

My so­lu­tion

Now we went through a the­o­retic so­lu­tion to cre­ate type-vari­ant con­tain­ers, I’ll show you my so­lu­tion.

The code is re­lease un­der LGPL 2.0 or later, it means you can reuse it even in pro­pri­etary soft­ware. You’ll just need to keep the header and to send me your mod­i­fi­ca­tions that I may add to the ar­ti­cle in re­turn.

I de­cided not to cre­ate a shared li­brary be­cause of the small amount of code in there. It would add un­nec­es­sary com­plex­ity to your build­ing sys­tem as it would add an ex­tra de­pen­dency.

What do you think of this so­lu­tion ? Feel free to leave com­ments.

Comments