embedded software boot camp

Effective C Tip #7 – Use strongly typed function parameters

Monday, October 12th, 2009 by Nigel Jones

This is the seventh in a series of tips on writing effective C. Today’s topic concerns function parameters, and more to the point, how you should choose them in order to make your code considerably more resilient to parameter passing errors.ย  What do I mean by parameter passing errors? Well consider a function that is intended to draw a rectangle on a display. The lousy way to design this function interface would be something like this:

void draw_rect(int x1, int y1, int x2, int y2, int color, int fill)
{
...
}

I must have seen a function like this many times. So what’s wrong with this you ask? Well in computer jargon the parameters are too weakly typed. To put it into plain English, it’s way too easy to pass a Y ordinate when you are supposed to pass an X ordinate, or indeed to pass a color when you are supposed to be passing an ordinate or a fill pattern. Although in this case (and indeed in most cases) these types of mistakes are clearly discernible at run time, I’m a firm believer in catching as many problems at compile time as possible. So how do I do this? Well there are various things one can do. The most powerful technique is to use considerably more meaningful data types. In this case, I’d do something like this:

typedef struct
{
 int x;
 int y;
} COORDINATE;

typedef enum
{
 Red, Black, Green, Purple .... Yellow
} COLOR;

typedef enum
{
 Solid, Dotted, Dashed .. Morse
} FILL_PATTERN;

void draw_rect(COORDINATE p1, COORDINATE p2, COLOR color, FILL_PATTERN fill)
{
...
}

Now clearly it’s highly likely that your compiler will complain if you attempt to pass a coordinate to a color and so on – and thus this is a definite improvement. However, nothing I’ve done here will prevent the X & Y ordinates being interchanged. Unfortunately, most of the time you are out of luck on this one – except in the case where you are dealing with certain sizes of display panels with resolutions such as 320 * 64, 320 * 128 and so on. In these cases, the X ordinate must be represented by a uint16_t whereas the Y ordinate may be represented by a uint8_t. In which case my COORDINATE data type becomes:

typedef struct
{
uint16_t x;
uint8_t y;
} COORDINATE;

This will at least cut down on the incidence of parameters being passed incorrectly.

Although you probably will not get much help from the compiler, you can also often get a degree of protection by declaring appropriate parameters as const. A good example of this is the standard C function memcpy(). If like me, you find yourself wondering if it’s memcpy(to, from) or memcpy(from, to), then an examination of the function prototype tells you all you need to know:

void *memcpy(void * s1, const void * s2, size_t n);

That is, the first parameter is simply declared as a void * pointer, whereas the second parameter is declared as void * pointer to const. In short the second parameter points to what we are reading from, and hence memcpy is indeed memcpy(to, from). Now I’m sure that many of you are thinking to yourself – so what, the real solution to this is to give meaningful names to the function prototype. For example:

void *memcpy(void *destination, const void *source, size_t nos_bytes);

Although I agree wholeheartedly with this sentiment, I’ll make two observations:

  1. You are assuming that the person reading your code is sufficiently fluent in the language (English in this case) that the names are meaningful to them.
  2. Your idea of a meaningful label may not be shared by others. I’ve noticed that this is particularly the case with software, as it seems that all too often the ability to write code and the ability to put a meaningful sentence together are inversely correlated.

The final technique that I employ concerns psychology!ย  Now one can argue that the failure to pass parameters correctly is due to laziness on behalf of the caller. At the end of the day, this is indeed the case. However, I suspect that in many cases, it’s not because the caller was lazy, but rather it’s because the caller thought they knew what the function parameter ordering is (or should be). A classic example of this of course concerns dates. Being from the UK (or more relevantly – Europe), I grew up thinking of dates as being day / month / year. Here in the USA, they of course use the month / day / year format. Thus when designing a function that needs to be passed the day, month and year, in what order should one declare the parameters? Well in my opinion it’s year, month, day. That is the function should look like this:

void foo(int16_t year, MONTH month, uint8_t day)

There are several things to note:

  1. By putting the year first, one causes both Europeans and Americans to think twice. This is where the psychology comes in!
  2. I’ve made the year signed – because it can indeed be negative, whereas the month and day cannot.
  3. I’ve made the month a MONTH data type, thus considerably increasing the likelihood that an attempt to pass a day when a month is required will be flagged by the compiler.
  4. I’ve made the day yet another data type (that maps well on to its expected range). Furthermore, attempts to pass most year values to this parameter will result in a compilation warning.

Thus I’ve used a combination of psychology and good coding practice to achieve a more robust function interface.
Thus the bottom line when it comes to designing function interfaces:

  1. Use strongly typed parameters.
  2. Use const where you can.
  3. Don’t assume that what is ‘natural’ to you is ‘natural’ to everyone.
  4. Do indeed use descriptive parameter names – but don’t assume that everyone will understand them.
  5. Apply some pop psychology if necessary.

I hope you find this useful.

Next Tip

Previous Tip

Home

15 Responses to “Effective C Tip #7 – Use strongly typed function parameters”

  1. Gauthier says:

    An only vaguely related note about date order: YYMMDD is the usual format in Sweden (and I probably the rest of scandinavia). In this format, number order and chronological order is the same, which turn out quite practical on a computer.I saw once this comment in code I took over:// from a meeting with XXXX 2003-13-14go figure if it's january or februari 2004 ๐Ÿ™‚ .

  2. Nigel Jones says:

    I've been to Sweden probably six times in the last few years and I never noticed that you use the YYMMDD format. My apologies to Scandinavia! I think it does illustrate the perils of assuming something though!

    • Anders says:

      “ISO 8601 Data elements and interchange formats โ€” Information interchange โ€” Representation of dates and times”
      ๐Ÿ™‚

  3. Mans says:

    Scandinavia is big endian, Britain is little endian, and the US is middle endian.If a function takes many parameters, putting them in a struct and passing a pointer is often more efficient (as well as being more robust) since it avoids copying the values on and off the stack, particularly if the same, or mostly the same, values are reused over many calls.

  4. Gauthier says:

    I stumble upon this article again: I wonder why your enum types are in all caps? I thought you liked having all caps for constants, and names ending with “_t” for types. Surely, COLOR, COORDINATE, and FILL_PATTERN are more types than constants?

    And this leads to another remark: the values in the enum are in fact constants. My standards (and Michael Barr’s, correct me if I’m wrong) are not really explicit on what convention to use there, but since the elements of an enum are constants I use all caps.

    Of course, it’s only a matter of convention, I’m just a bit surprised by yours.

    • Nigel Jones says:

      I tend to think of enumerated values as ‘typed constants’. Like you because they are constant I think they should be capitalized. The type side of enumerations I don’t usually worry about too much, in part because I have Lint set up to complain loudly about questionable type conversions involving enumerated types.

  5. Dirk says:

    Hi Nigel!
    I learned a lot from your articles, thank you very much!
    So after reading this article about strongly typed parameters, I stumbled upon a problem with typedefs as function parameters. See this code:

    typedef char[16] SERIAL_STR;

    void test_func(SERIAL_STR param)
    {
    SERIAL_STR local_var;
    printf(“sizeof(param)=%u\n”,sizeof(param));
    printf(“sizeof(SERIAL_STR)=%u\n”,sizeof(SERIAL_STR));
    printf(“sizeof(local_var)=%u\n”,sizeof(local_var));
    }

    On a 16bit dsPIC the output is:
    sizeof(param)=2
    sizeof(SERIAL_STR)=16
    sizeof(local_var)=16

    When the typedef is a function parameter, it seems to be treated like a pointer by the compiler.

    • Nigel Jones says:

      I’m glad you find the articles useful. While a typedef allows you to create a shorthand for a data type, it doesn’t allow you to break the language rules. In C you can’t pass an array to a function by value – only by reference. Thus given that SERIAL_STR is typedef’d to be char[16], the compiler is simply converting your attempt to pass by value into a pass by reference – i.e. a pointer

      • Dirk says:

        But wouldn’t it be more reasonable to give a compile error? It seems wrong to me that they are different although they are “declared” in the same way.

        • Nigel Jones says:

          The same thought had crossed my mind as well. I just dropped the code into my IAR ARM compiler. It gives the following errors on your typedef:
          Error[Pe040]: expected an identifier test.c 114
          Error[Pe065]: expected a “;” test.c 114
          What compiler are you using?

          • Dirk says:

            I’m using the Microchip C compiler for dsPIC30 (based on gcc version 4.0.3).

          • Nigel Jones says:

            Hmmm, that may well explain it. I normally find GCC to be very good on issues such as this, so perhaps it’s something Microchip has done as a ‘special extension’?

    • Jรถrg Seebohn says:

      The typedef SERIAL_STR is not standard:

      You should declare it like:

      typedef char SERIAL_STR[16] ;

      Every array parameter is treated as a pointer parameter.

      But there is a trick if you want to let the compiler determine the correct array size:

      void test_func(SERIAL_STR * param)
      {
      printf(“sizeof(*param)=%u”, sizeof(*param)) ;
      }

      // prints sizeof(*param)=16

      • Nigel Jones says:

        That’s very interesting Jorg. I don’t recollect having seen this before.

      • Brendan Simon says:

        Or use

        typedef struct
        {
        char c[16];
        } SERIAL_STR;

        // or
        //
        // typedef struct serial_str_s
        // {
        // char c[16];
        // } serial_str_t;

        SERIAL_STR mystr;

        //sizeof(SERIAL_STR) = 16

        // sizeof(mystr) = 16

        I like to use this as it makes the object look like a first class object. So to pass the address of the object you have to use & operator, like passing any other struct, instead of passing just the name of the array (effectively a pointer).

        some_func(&mystr);

        Maybe it’s the same thing as above ??

Leave a Reply to Nigel Jones

You must be logged in to post a comment.