Reputation: 1584
I'm taking over code from another person. I found a coding pattern that seems odd to me. This is for an embedded 32 bit PIC processor, in case that's relevant.
They have a custom typedef in a header file similar to the following:
typedef struct {
custom_object obj;
char** list_text;
uint8_t length;
void (*func)(void* this_obj);
//other variables here
} custom_list;
But whenever this custom_list type is passed to a function, the function is defined like this:
void custom_function(void* this_obj) {
custom_list *list = (custom_list*) this_obj;
//miscellaneous code here...
}
So, they read in the variable as pointer to void, then recast it as pointer to custom_list
inside the function.
Note: also added the func
element to the definition above and clarified some wording
There are two ways I've found that this function is called.
When inside of one of these functions and calling another one, it is called by directly passing the void pointer version of the object, this_obj
:
custom_function(this_obj);
These functions are normally called via a pointer within the very object that is getting passed (e.g. list.func = &custom_function
). The functions are being used as a pseudo-method in a pseudo-class I guess. So, the calls typically go:
list->func((void*)list)
where list
is of type custom_list
Note that for case 1, previous operations before the call use the casted custom_list
pointer variable list
to make changes to its elements (e.g. list->length = X
).
Since both variables are "just" pointers, it's the same memory location being changed, but it makes it difficult to follow the code since it APPEARS that one variable is manipulated but another is passed to the next function.
As a side note... in case it's also relevant. To not get into debate about global variables, I left out that char** list is used to point to a GLOBAL list variable pointer. In these functions, that global list is manipulated, rather than performing actions on the pointer within the struct / object itself. I believe they did this in an attempt to save memory. Only one object of custom_list is used at a time. Before another custom_list object is used, the global list is cleared with free
, then the next object puts new data in the list using malloc
. Personally, I find this makes this code really hard to follow.
Why not just pass as type pointer to custom_list, like so?
void custom_function(custom_list* list) {
//miscellaneous code here...
}
Isn't passing it as pointer to void then recasting it extra steps? Wouldn't this be a bad practice... were they doing this just to work around a compiler checker error? Or am I missing something, is there some advantage or necessity to the first approach?
Upvotes: 2
Views: 149
Reputation: 835
There can be multiple reasons for this. For example the function could be used as an argument for another more general function somewhere.
Here is a simple example, which is not exactly practical C code, but is easy to understand. It is a simple implementation of a map function, applying a given function to every element in an array.
#include <stdio.h>
#include <ctype.h>
void map(int len, void** array, void (*operator)(void*)) {
for (int i = 0; i < len; i++)
operator(array[i]);
}
void caesar_encode(void* val) {
char* str = val;
for (int i = 0; str[i]; i++) {
if (!isalpha(str[i]))
continue;
char offset = islower(str[i]) ? 'a' : 'A';
str[i] = (str[i] - offset + 4) % 26 + offset;
}
}
void main(int argc, char** argv) {
map(argc - 1, (void**) argv + 1, caesar_encode);
for (int i = 1; i < argc; i++)
printf("%s ", argv[i]);
printf("\n");
}
This program applies the Caesar encoding to every alphabetic character in the arguments passed to this program.
A practical example of something like this is pthread_create used to create a new thread object.
pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg)
The function the thread starts with has to have the signature void *(*start_routine)(void *)
allowing you to pass any data you like (packed in a struct for example).
The other reason could be that the person who wrote the code did not like opaque pointer types. A header could look something like this:
...
typedef struct custom_list* CustomList;
CustomList create_custom_list();
void destroy_custom_list(CustomList);
...
So maybe the person who wrote the code simply did not like opaque pointer types and is not used to them or the code came from a time before they were a thing. I am just speculating here at this point.
Upvotes: 1
Reputation: 224072
The way the struct is defined:
typedef struct {
custom_object obj;
char** list_text;
uint8_t length;
void (*func)(void* this_obj);
//other variables here
} custom_list;
The func
member can't specify a parameter of type custom_list
because the type hasn't yet been defined.
The proper way to do this would be to add a tag to the struct. Then a pointer to it can be referenced in the struct definition:
typedef struct _custom_list {
custom_object obj;
char** list_text;
uint8_t length;
void (*func)(struct _custom_list* this_obj);
//other variables here
} custom_list;
Then the function that would be assigned to this pointer can be defined with the proper type:
void custom_function(custom_list* list) {
//miscellaneous code here...
}
Upvotes: 1
Reputation: 1584
In context, this now makes sense. With the update, showing more of the members of the structure:
typedef struct {
custom_object obj;
char** list_text;
uint8_t length;
void (*func)(void* this_obj);
//other variables here
} custom_list;
If the function pointed to by func
, in this case custom_function, takes a parameter of type custom list
you can't reference that type within the definition of the type. That is to say, if it were written:
typedef struct {
custom_object obj;
char** list_text;
uint8_t length;
void (*func)(custom_list* this_obj);
//other variables here
} custom_list;
It will fail with error: unknown type name 'custom_list'
because it doesn't know what custom_list
is yet because it hasn't actually been fully defined.
This is an attempt to basically make a C++ like class, or pseudo-class in C. C++ classes assume an object of the class type is one of the inputs to any member methods without even having to explicitly state it. In C you have to do it as a void pointer to the structure and you just have make sure that you're pointing to the correct type in your code as now it can't be checked.
Upvotes: 0