so.very.tired
so.very.tired

Reputation: 3086

Type-safety in C

Is there a way to make C a little more aware of types and assure type-safety?
Consider this:

typedef unsigned cent_t;
typedef unsigned dollar_t;

#define DOLLAR_2_CENT(dollar)       ((cent_t)(100*(dollar)))

void calc(cent_t amount) {
    // expecting 'amount' to semantically represents cents...
}

int main(int argc, char* argv[]) {
    dollar_t amount = 50;
    calc(DOLLAR_2_CENT(amount));  // ok
    calc(amount);                 // raise warning
    return 0;
}

Is there a way to make the above code at-least raise warning by the gcc?
I know I can use C-structs to wrap unsigneds and achieve the desired result, I was just wondering if there was a more elegant way to do it.
Can it be a little more than that?

Upvotes: 39

Views: 7955

Answers (5)

Raffaello
Raffaello

Reputation: 1706

C is not type safe, if you want type safe you have to choose another language, there is no workaround of it, and try to do a "type safe C" is like try to program in another language. And C is good as it is, because in ASM/machine code/cpu instructions there is no type safety construct, and C is quite close to it, but in that perspective is a high level language.

So also back at yuor example, rather than having 2 unsigned, it would be enough to have a fixed point number that you can simply emulate with * 100 so basically $1.00 is stored in your number as 100.

Also not sure if unsigned could be striclty the right choice.

Furthermore if you want a more fancy not required high-level construct you can use a struct of 2 values for the amount and the cents. wasting twice the memory requirement for no real more information. But i will just suggest that you should go with a fixed point type

Finally going more on your question: use the compiler and enable all the warnings and force them as error. And yes as other wrote already get also the help of some static analysis tools.

But what are you looking for is not the C language.

Upvotes: 1

Patrick Schlüter
Patrick Schlüter

Reputation: 11871

EDIT: Here an alternative that works even in C89, in case your compiler doesn't support _Generic selector's (a lot of compilers don't and often you're stuck with what's installed on your machine).

You can use macros to simplify the use of struct wrappers.

#define NEWTYPE(nty,oty) typedef struct { oty v; } nty
#define FROM_NT(ntv)       ((ntv).v)
#define TO_NT(nty,val)     ((nty){(val)})  /* or better ((nty){ .v=(val)}) if C99 */


NEWTYPE(cent_t, unsigned);
NEWTYPE(dollar_t, unsigned);

#define DOLLAR_2_CENT(dollar)       (TO_NT(cent_t, 100*FROM_NT(dollar)))

void calc(cent_t amount) {
     // expecting 'amount' to semantically represents cents...
}  

int main(int argc, char* argv[]) {
    dollar_t amount = TO_NT(dollar_t, 50);  // or alternatively {50};
    calc(DOLLAR_2_CENT(amount));  // ok
    calc(amount);                 // raise warning
    return 0;
}

You get even stronger than a warning. Here is the compilation result with gcc 5.1

$ gcc -O3  -Wall Edit1.c
Edit1.c: In function ‘main’:
Edit1.c:17:10: error: incompatible type for argument 1 of ‘calc’
     calc(amount);                 // raise warning
          ^
Edit1.c:10:6: note: expected ‘cent_t {aka struct }’ but argument is of type ‘dollar_t {aka struct }’
 void calc(cent_t amount);// {

and here the result with gcc 3.4

$ gcc -O3  -Wall Edit1.c
Edit1.c: In function 'main':
Edit1.c:17: error: incompatible type for argument 1 of 'calc'

Upvotes: 7

Andy Sinclair
Andy Sinclair

Reputation: 106

You need to use a static analysis tool in your build process to achieve this.

For example, if you run PCLint on your code, it gives this output:

  [Warning 632] Assignment to strong type 'cent_t' in context: arg. no. 1
  [Warning 633] Assignment from a strong type 'dollar_t' in context: arg. no. 1

http://www.gimpel.com/html/strong.htm

Upvotes: 9

Lundin
Lundin

Reputation: 215114

The problem is that C doesn't treat your two typedefs as distinctive types, because they are both type unsigned.

There are various tricks to dodge this. One thing would be to change your types to enums. Good compilers will enforce stronger typing warnings on implicit conversions to/from a certain enum type to any other type.

Even if you don't have a good compiler, with enums you could do this:

typedef enum { FOO_CENT  } cent_t;
typedef enum { FOO_DOLLAR} dollar_t;

#define DOLLAR_2_CENT(dollar)       ((cent_t)(100*(dollar)))

void calc(cent_t amount) {
    // expecting 'amount' to semantically represents cents...
}

#define type_safe_calc(amount) _Generic(amount, cent_t: calc(amount))

int main(int argc, char* argv[]) {
    dollar_t amount = 50;
    type_safe_calc(DOLLAR_2_CENT(amount));  // ok
    type_safe_calc(amount);         // raise warning

    return 0;
}

A more conventional/traditional trick is to use a generic struct wrapper, where you use a "ticket" enum to mark the type. Example:

typedef struct
{
  type_t type;
  void*  data;
} wrapper_t;

...

cent_t my_2_cents;
wrapper_t wrapper = {CENT_T, &my_2_cents};

...

switch(wrapper.type)
{
  case CENT_T: calc(wrapper.data)
  ...
}

The advantage is that it works with any C version. Disadvantage is code and memory overhead, and that it only allows run-time checks.

Upvotes: 31

n. m. could be an AI
n. m. could be an AI

Reputation: 120079

Aliasing has a very specific narrow meaning in C, and it's not what you have in mind. You may want to say "typedefing".

And the answer is no, you can't. Not in an elegant way at any rate. You can use a struct for each numeric type, and a separate set of functions to do arithmetic with each one. Except when it comes to multiplication, you are out of luck. In order to multiply feet by pounds, you need a third type. You also need types for feet squared, feet cubed, seconds to the power of minus two and an infinite number of other types.

If this is what you are after, C is not the right language.

Upvotes: 10

Related Questions