Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a generic function in C, how to handle strings

Tags:

c

I have a function that takes a void** argument and an integer that indicates its datatype

void foo (void** values, int datatype)

Inside the function, depending on the datatype, I malloc it this way:

if (datatype == 1)
    *values = (int*) malloc (5 * sizeof(int));
else if (datatype == 2)
    *values = (float*) malloc (5 * sizeof(float));

All is good upto now. However, when character strings come into the picture, things get complicated. The void** would need to be void***, since I will need to do something like this:

*values = (char**) malloc (5 * sizeof(char*));
for(i=0;i<5;i++)
    (*values)[i] = (char*) malloc (10);
..
strncpy( (*values)[0], "hello", 5);

How should such a situation be handled? Can I pass a char*** to the function that expects a void** but cast it correctly inside it?

void foo (void** values, int datatype) {

if(datatype == 3) {
    char*** tmp_vals = (char***) values;
    *tmp_vals = (char**) malloc (5 * sizeof(char*));
    ...
    (*tmp_vals)[i] = (char*) malloc (10 * sizeof(char));
    strncpy (  (*tmp_vals)[i], "hello", 5);
}

So I just cast the void** into a char***. I tried this and ignoring the warnings, it worked fine. But is this safe? Is there a more graceful alternative?

like image 724
jitihsk Avatar asked Oct 03 '13 22:10

jitihsk


2 Answers

How should such a situation be handled? Can I pass a char*** to the function that expects a void** but cast it correctly inside it?

No, that's technically Undefined Behavior. It may appear to work on your computer, but it may fail on some future computer that implements different pointer types with different representations, which is allowed by the C language standard.

If your function expects a void**, then you better pass it a void**. Any pointer type can be implicitly converted to void*, but that only works at the top level: char* can be converted to void*, and char** can be implicitly converted to void* (because char** is "pointer to char*"), but char** cannot be converted to void**, and likewise char*** also cannot be converted to void**.

The proper way to call this function is to pass it a proper void**, then cast the resulting void* pointer back to its original type:

void foo(void **values, int datatype)
{
    if(datatype == 3)
    {
        char ***str_values = ...;
        *values = str_values;  // Implicit cast from char*** to void*
    }
    else
    ...
}

...

void *values;
foo(&values, 2);
char ***real_values = (char ***)values;

Assuming that *values was actually pointed to a char***, then this cast is valid and does not have any Undefined Behavior in any of the code paths.

like image 172
Adam Rosenfield Avatar answered Oct 24 '22 22:10

Adam Rosenfield


A void * is just a pointer to an unspecified type; it could be a pointer to an int, or a char, or a char *, or a char **, or anything you wanted, as long as you ensure that when you dereference, you treat it as the appropriate type (or one which the original type could safely be interpreted as).

Thus, a void ** is just a pointer to a void *, which could be a pointer to any type you want such as a char *. So yes, if you are allocating arrays of some types of objects, and in one case those objects are char *, then you could use a void ** to refer to them, giving you something that could be referred to as a char ***.

It's generally uncommon to see this construction directly, because usually you attach some type or length information to the array, rather than having a char *** you have a struct typed_object **foo or something of the sort where struct typed_object has a type tag and the pointer, and you cast the pointer you extract from those elements to the appropriate types, or you have a struct typed_array *foo which is a struct that contains a type and an array.

A couple of notes on style. For one, doing this kind of thing can make your code hard to read. Be very careful to structure it and document it clearly so that people (including yourself) can figure out what's going on. Also, don't cast the result of malloc; the void * automatically promotes to the type its assigned to, and casting the result of malloc can lead to subtle bugs if you forget to include <stdlib.h> or your update the type declaration but forget to update the cast. See this question for more info.

And it's generally a good habit to attach the * in a declaration to the variable name, not the type name, as that's how it actually parses. The following declares one char and one char *, but if you write it the way you've been writing them, you might expect it to declare two char *:

char *foo, bar;

Or written the other way:

char* foo, bar;
like image 34
Brian Campbell Avatar answered Oct 24 '22 22:10

Brian Campbell