I would like to use this to implement stuff like passing in a NULL error object to a function, and if there is an error, it sets the pointer of the error to some error code, etc.
From the above quote and from the code in the question, it seems you are looking for a variable that can "hold" different types, i.e. sometimes you want it to be an integer, at other times a float, at other times a string and so on. This is called a variant in some languages but variants doesn't exist in C. (see this https://en.wikipedia.org/wiki/Variant_type for more about variants)
So in C you'll have to code your own variant type. There are several ways to do that. I'll give examples below.
But first a few words on pointers in C because the code in the question seem to reveal a misunderstanding as it assigns values directly to the pointer, e.g. pointer = somestruct;
which is illegal.
In C is very important to understand the difference between the "value of a pointer" and the "value of the pointed to object". The first, i.e. value of a pointer, tells where the pointer is pointing, i.e. the value of a pointer is the address of the pointed to object. Assignments to a pointer changes where the pointer is pointing. To change the value of the pointed to object, the pointer must be dereferenced first. Example (pseudo code):
pointer = &some_int; // Make pointer point to some_int
*pointer = 10; // Change the value of the pointed to object, i.e. some_int
// Notice the * in front of pointer - it's the dereference
// that tells you want to operate on the "pointed to object"
pointer = 10; // Change the value of the pointer, i.e. where it points to
// In other words, pointer no longer points to some_int
Now back to the "variant" implementation. As already mentioned there are several ways to code that in C.
From your question it seems that you want to use a void-pointer. It's doable and I'll start by showing an example using void-pointer and after that an example using a union.
It's not clear in your question what cond
are so in my examples I'll just assume it's a command line argument and I just added some interpretation in order to have a running example.
The common pattern for the examples is the use of a "tag". That is an extra variable that tells the current type of objects value (aka meta-data). So the general variant data type looks like:
struct my_variant
{
TagType tag; // Tells the current type of the value object
ValueType value; // The actual value. ValueType is a type that allows
// storing different object types, e.g. a void-pointer or a union
}
Example 1 : void-pointer and casts
The example below will use a void-pointer to point to the object containing the real value. A value that sometimes is an integer, sometimes a float or whatever is needed. When working with a void-pointer, it's necessary to cast the void-pointer before dereferencing the pointer (i.e. before accessing the pointed to object). The tag
field tells the type of the pointed to object and thereby also how the cast shall be.
#include <stdio.h>
#include <stdlib.h>
// This is the TAG type.
// To keep the example short it only has int and float but more can
// be added using the same pattern
typedef enum
{
INT_ERROR_TYPE,
FLOAT_ERROR_TYPE,
UNKNOWN_ERROR_TYPE,
} error_type_e;
// This is the variant type
typedef struct
{
error_type_e tag; // The tag tells the type of the object pointed to by value_ptr
void* value_ptr; // void pointer to error value
} error_object_t;
// This function evaluates the error and (if needed)
// creates an error object (i.e. the variant) and
// assigns appropriate values of different types
error_object_t* get_error_object(int err)
{
if (err >= 0)
{
// No error
return NULL;
}
// Allocate the variant
error_object_t* result_ptr = malloc(sizeof *result_ptr);
// Set tag value
// Allocate value object
// Set value of value object
if (err > -100) // -99 .. -1 is INT error type
{
result_ptr->tag = INT_ERROR_TYPE;
result_ptr->value_ptr = malloc(sizeof(int));
*(int*)result_ptr->value_ptr = 42;
}
else if (err > -200) // -199 .. -100 is FLOAT error type
{
result_ptr->tag = FLOAT_ERROR_TYPE;
result_ptr->value_ptr = malloc(sizeof(float));
*(float*)result_ptr->value_ptr = 42.42;
}
else
{
result_ptr->tag = UNKNOWN_ERROR_TYPE;
result_ptr->value_ptr = NULL;
}
return result_ptr;
}
int main(int argc, char* argv[])
{
if (argc < 2) {printf("Missing arg\n"); exit(1);}
int err = atoi(argv[1]); // Convert cmd line arg to int
error_object_t* err_ptr = get_error_object(err);
if (err_ptr == NULL)
{
// No error
// ... add "normal" code here - for now just print a message
printf("No error\n");
}
else
{
// Error
// ... add error handler here - for now just print a message
switch(err_ptr->tag)
{
case INT_ERROR_TYPE:
printf("Error type INT, value %d\n", *(int*)err_ptr->value_ptr);
break;
case FLOAT_ERROR_TYPE:
printf("Error type FLOAT, value %f\n", *(float*)err_ptr->value_ptr);
break;
default:
printf("Error type UNKNOWN, no value to print\n");
break;
}
free(err_ptr->value_ptr);
free(err_ptr);
}
return 0;
}
Some examples of running this program:
> ./prog 5
No error
> ./prog -5
Error type INT, value 42
> ./prog -105
Error type FLOAT, value 42.419998
> ./prog -205
Error type UNKNOWN, no value to print
As the example above shows, you can implement a variant type using void-pointer. However, the code requires a lot of casting which makes the code hard to read. In general I'll not recommend this approach unless you have some special requirements that forces the use of void-pointer.
Example 2 : pointer to union
As explained earlier C doesn't have variants as they are known in other languages. However, C has something that is pretty close. That is unions. A union can hold different types at different times - all it misses is a tag
. So instead of using a tag and a void-pointer, you can use a tag and a union. The benefit is that 1) casting will not be needed and 2) a malloc
is avoided. Example:
#include <stdio.h>
#include <stdlib.h>
typedef enum
{
INT_ERROR_TYPE,
FLOAT_ERROR_TYPE,
UNKNOWN_ERROR_TYPE,
} error_type_e;
// The union that can hold an int or a float as needed
typedef union
{
int n;
float f;
} error_union_t;
typedef struct
{
error_type_e tag; // The tag tells the current union use
error_union_t value; // Union of error values
} error_object_t;
error_object_t* get_error_object(int err)
{
if (err >= 0)
{
// No error
return NULL;
}
error_object_t* result_ptr = malloc(sizeof *result_ptr);
if (err > -100) // -99 .. -1 is INT error type
{
result_ptr->tag = INT_ERROR_TYPE;
result_ptr->value.n = 42;
}
else if (err > -200) // -199 .. -100 is FLOAT error type
{
result_ptr->tag = FLOAT_ERROR_TYPE;
result_ptr->value.f = 42.42;
}
else
{
result_ptr->tag = UNKNOWN_ERROR_TYPE;
}
return result_ptr;
}
int main(int argc, char* argv[])
{
if (argc < 2) {printf("Missing arg\n"); exit(1);}
int err = atoi(argv[1]); // Convert cmd line arg to int
error_object_t* err_ptr = get_error_object(err);
if (err_ptr == NULL)
{
// No error
// ... add "normal" code here - for now just print a message
printf("No error\n");
}
else
{
// Error
// ... add error handler here - for now just print a message
switch(err_ptr->tag)
{
case INT_ERROR_TYPE:
printf("Error type INT, value %d\n", err_ptr->value.n);
break;
case FLOAT_ERROR_TYPE:
printf("Error type FLOAT, value %f\n", err_ptr->value.f);
break;
default:
printf("Error type UNKNOWN, no value to print\n");
break;
}
free(err_ptr);
}
return 0;
}
In my opinion this code is easier to read than the code using void-pointer.
Example 3 : union - no pointer - no malloc
Even if example 2 is better than example 1 there is still dynamic memory allocation in example 2. Dynamic allocation is part of most C programs but it is something that shall be used only when really needed. In other words - objects with automatic storage duration (aka local variables) shall be prefered over dynamic allocated objects when possible.
The example below shows how the dynamic allocation can be avoided.
#include <stdio.h>
#include <stdlib.h>
typedef enum
{
NO_ERROR,
INT_ERROR_TYPE,
FLOAT_ERROR_TYPE,
UNKNOWN_ERROR_TYPE,
} error_type_e;
typedef union
{
int n;
float f;
} error_union_t;
typedef struct
{
error_type_e tag; // The tag tells the current union usevalue_ptr
error_union_t value; // Union of error values
} error_object_t;
error_object_t get_error_object(int err)
{
error_object_t result_obj;
if (err >= 0)
{
// No error
result_obj.tag = NO_ERROR;
}
else if (err > -100) // -99 .. -1 is INT error type
{
result_obj.tag = INT_ERROR_TYPE;
result_obj.value.n = 42;
}
else if (err > -200) // -199 .. -100 is FLOAT error type
{
result_obj.tag = FLOAT_ERROR_TYPE;
result_obj.value.f = 42.42;
}
else
{
result_obj.tag = UNKNOWN_ERROR_TYPE;
}
return result_obj;
}
int main(int argc, char* argv[])
{
if (argc < 2) {printf("Missing arg\n"); exit(1);}
int err = atoi(argv[1]); // Convert cmd line arg to int
error_object_t err_obj = get_error_object(err);
switch(err_obj.tag)
{
case NO_ERROR:
printf("No error\n");
break;
case INT_ERROR_TYPE:
printf("Error type INT, value %d\n", err_obj.value.n);
break;
case FLOAT_ERROR_TYPE:
printf("Error type FLOAT, value %f\n", err_obj.value.f);
break;
default:
printf("Error type UNKNOWN, no value to print\n");
break;
}
return 0;
}
Summary
There are many ways of solving the problem addressed by OP. Three examples have been given in this answer. In my opinion example 3 is the best approach as it avoids dynamic memory allocation and pointers but there may be situations where example 1 or 2 is better.