Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » Passing arguments of unknown type to functions in C

This thread is locked; no one can reply to it. rss feed Print
Passing arguments of unknown type to functions in C
kenmasters1976
Member #8,794
July 2007

Following my previous thread, I'm now trying to use the result of an expression evaluation as argument to a function call. The expression is read from an external file or the command line, and it's evaluated at every frame of my game; the actual type of the argument is not unknown as the thread title suggests but it is only known until after the expression has been evaluated. I'm storing the result of the evaluation in an struct like:

typedef struct T {
  enum {INT, FLOAT, POINTER} type;
  union {
    int   i;
    float f;
    void *p;
  };
} T;

Now, imagine I have some expressions I want to use as arguments to a function; after evaluating the expressions, I might have something like:

T t1, t2, t3;

Is there a way to call some function with the right argument depending on the type specified in the struct? That is, something like in the following pseudo code:

     if (t1.type == INT)     ARG1 = t1.i;
else if (t1.type == FLOAT)   ARG1 = t1.f;
else if (t1.type == POINTER) ARG1 = t1.p;
     if (t2.type == INT)     ARG2 = t2.i;
else if (t2.type == FLOAT)   ARG2 = t2.f;
else if (t2.type == POINTER) ARG2 = t2.p;
     if (t3.type == INT)     ARG3 = t3.i;
else if (t3.type == FLOAT)   ARG3 = t3.f;
else if (t3.type == POINTER) ARG3 = t3.p;
my_function(ARG1, ARG2, ARG3);

At first, I thought of using a ternary operator to return the value but it didn't work for the same reason the pseudo code above wouldn't work in C, ARG1, ARG2 and ARG3 don't have a specific type.

Is it possible to do something like this in C?.

torhu
Member #2,727
September 2002
avatar

If the function can take arguments of varying types, why not pass them as T? If you are evaluating expressions I think the next step would be splitting it into subexpressions like addition, subtraction, dereference, etc., which would give you a bunch of functions like eval_add, eval_sub, eval_ref, etc. that take one or two arguments of more specific types.

EDIT: Or wait, I think the binary functions would still take T, but they would call something like get_value with a T, which would just return a double. eval_ref would be called by get_value, etc.

Peter Hull
Member #1,136
March 2001

I don't think it's possible in C. (it's called dispatch which might let you google for it)

I would write your functions to take parameters of T (or a pointer to them) and have the first few lines sort out what to do with the types. For example you might want to accept a FLOAT, coerce an INT to an float, and flag an error if it's a POINTER. Or you could call one of a set of 'sub-functions'. Pretty tiresome but that's the simplest way I think. An alternative would be some kind of table-based approach.
e.g. (pseudo-code)

void my_add_int(int i1, int i2);
void my_add_float(float f1, float f2);
void error();
void my_add(T t1, T t2) {
 if (t1.type == INT && t2.type == INT) { my_add_int(t1.i, t2.i); }
 else if (t1.type == INT && t2.type == FLOAT) { my_add_float((float) t1.i, t2.f); }
 else if (t1.type == FLOAT && t2.type == INT) { my_add_float(t1.f, (float) t2.i); }
 else if (t1.type == FLOAT && t2.type == FLOAT) { my_add_float(t1.f, t2.f); }
 else error();
}

Audric
Member #907
January 2001

I suspect you won't really have 27 variants of a function for all combinations of (int,int,int), (int,int,float), (int,int,pointer), (int,float,int) etc...

In case it's helpful for what you're doing, note that C lets you use an array of function pointers in order to compute the "right" function to call.
However this needs all variants to have the same signature, ie. T type. It's then up to each implementation to pick the right field (i, f or p)
And if it's not something you do often in code, this isn't a big improvement over your ifs.

For example (not 100% sure about syntax))

#SelectExpand
1T (*add_func[2]) (T arg1, T arg2); // prepare an array of 3 functions pointers, for functions that take two T args and return one T 2// fill it 3add_func[INT] = add_func_int; 4add_func[FLOAT] = add_func_float; 5add_func[POINTER] = add_func_int; 6 7T t1,T2; 8... 9add_func[t1.type](t1, t2); // perform the 'right' function based on t1's type 10 11... 12// implementation for int : this one uses 'i' field 13T add_func_int(T arg1, T arg2) 14{ 15 T val; 16 val.type = INT; 17 val.i = arg1.i + arg2.i 18 return val; 19}

kenmasters1976
Member #8,794
July 2007

Thank you all for your replies. While passing T as arguments is indeed the most obvious approach, I'd like to allow the code to be used with functions that don't have a dependency on the expression evaluation code. That is, imagine that I were to use that code as a library and I have a function like:

float do_something(float, int);

If the code required to pass T as arguments, I'd have to write a wrapper function:

T do_something_wrapper(T, T);

So that in an external file, after the proper setup, I could call it, for example as:

X = A * do_something_wrapper(B, C);

Instead, I'd like to call the original function as:

X = A * do_something(B, C);

That way, I don't need to write a wrapper function to every function I want to 'expose' to the expression evaluation library.

Audric said:

I suspect you won't really have 27 variants of a function...

In fact, my current approach consists of something like that. When 'exposing' a function to the expression evaluation code, I store the function pointer as well as a value that identifies the function signature so that I can use that value in a switch() statement as in:

switch (signature) {
  case INT_INT_INT:
    my_function(t1.i, t2.i, t3.i);
    break;
  case FLOAT_INT_INT:
    my_function(t1.f, t2.i, t3.i);
    break;
  ...
}

I know, this is not the most elegant code but the compiler optimizes the switch() statement, right? Also, the pointer can be cast into an intptr_t via a ternary operator (at the cost of a warning) so that reduces the actual number of cases to 2³ instead of 3³ in this case.

So yeah, this is the closest I've got and it requires a large selection of cases in a switch() and to pass the function signature when first exposing the function.

Audric
Member #907
January 2001

If you have a lot of such functions and argument variants, I would consider a system where each function, no matter its number or type of arguments, has the same signature. Data would be passed through a stack of T:
- the calling code stacks the arguments
- the callee unstacks, and can double-check that it was called with the correct types. Maybe using a kind of macro magic, for example the following:
(I let you judge if it's readable and/or useful for your scenario.)

#define UNSTACK_INT(var) if (args.type != INT) {return "error";} int var=args.i; args++;
#define UNSTACK_FLOAT(var) if (args.type != FLOAT) {return "error";} float var=args.f; args++;

// three args : int, int, float
char * func_myfunction_i_i_f(T *args)
{
UNSTACK_INT(i)
UNSTACK_INT(j)
UNSTACK_FLOAT(ratio)
....

}

kenmasters1976
Member #8,794
July 2007

I thought about using a stack but couldn't find a way to make it work. Now that I see your code, I can see how that macro magic works and makes a lot of sense, I think it will be definitely useful.

Thank you.

RmBeer2
Member #16,660
April 2017
avatar

I can guess that you are trying to build a processor emulator. :P

Any of the methods is fine, you just have to keep in mind that apart from the starting point in memory, you have to have a fixed size for the resulting variable, if the initial size is short, the resulting variable will have garbage in the last bytes.

The next problem is that you work with variables that are too different, like an 'int', a 'float', and a 'pointer', which you definitely can't combine all in one, each one suits a different use, which you need to create at least the same time 3 different functions for each of them. (It's the solution used by all emulators.)

🌈🌈🌈 🌟 BlackRook WebSite 🌟 C/C++ 🌟 GNU/Linux 🌟 IceCream/Cornet 🌟 🌈🌈🌈

Rm Beer for Emperor 2021!

Audric
Member #907
January 2001

My example was for showing an early return, but for a full expression system I think I would like error(s) to be a valid return value for an operator, ie. possible values for the enum in T.type
This would handle both mismatching parameters, and also divisions by zero or overflows.
Anytime an operand received an ERROR parameter, the return value is ERROR as well :
(3 + 5) * (2 / 0) + 1
8 * (2 / 0) + 1
8 * ERROR + 1
ERROR + 1
ERROR

RmBeer2, I'm not sure if you noticed the union in the T struct, it solves the variable size issue. Each instance of T has the same size, no matter what it holds.

kenmasters1976
Member #8,794
July 2007

RmBeer2 said:

You need to create at least the same time 3 different functions for each of them. (It's the solution used by all emulators.)

Interesting, RmBeer2. As Audric already mentioned, for the time being, the union holds all three types so at each step of the evaluation you can access the right field; for example, if I were to handle the division of two integers as in C, I would do:

t1.i = integer_operand_1 / integer_operand_2;
t1.type = INT;

If instead I wanted to let the division of two integers return a float, I would do:

t1.f = integer_operand_1 * 1.0 / integer_operand_2;
t1.type = FLOAT;

The operands themselves can be of type T so you can access the proper value; dividing by a pointer could be an error or could be a legal operation after casting it to int, it all depends on how the evaluation code is written. But still, it's a good thing to keep in mind.

Audric said:

Anytime an operand received an ERROR parameter, the return value is ERROR as well.

Yeah, that seems like a reasonable way to handle errors.

Thanks.

RmBeer2
Member #16,660
April 2017
avatar

@Audric , @kenmasters1976
'union' is just the initial solution. But then you are going to have problems like the one I already mentioned. Minimum multiple conditions like 'if' or 'switch'. Like the one you have already exposed in the first comment of the thread, only that ARG1/2/3 should also have 'struct T', or be of individual types 'int', 'float', 'pointer' after the multiple conditions.

Another thing would be to add a new solution like the C++ templates. But internally it would do the same, during compilation it would create a separate function for each of the types.

🌈🌈🌈 🌟 BlackRook WebSite 🌟 C/C++ 🌟 GNU/Linux 🌟 IceCream/Cornet 🌟 🌈🌈🌈

Rm Beer for Emperor 2021!

Go to: