This sort of thing is possible with a bit of work. First it's important to understand why something simpler is not possible: in C/C++, the exact mechanism by which arguments are passed to functions and how return values are obtained from the function depends on the types (and sizes) of the arguments. This is defined in the application binary interface (ABI) which is a set of conventions that allow C++ code compiled by different compilers to interoperate. The language also specifies a bunch of implicit type conversions that occur at the call site. So the short and simple answer is that in C/C++ the compiler cannot emit machine code for a call to a function whose signature is not known at compile time.
Now, you can of course implement something like Javascript or Python in C++, where all values (relevant to these functions) are typed dynamically. You can have a base "Value" class that can be an integer, float, string, tuples, lists, maps, etc. You could use std::variant
, but in my opinion this is actually syntactically cumbersome and you're better of doing it yourself:
enum class Type {integer, real, str, tuple, map};
struct Value
{
// Returns the type of this value.
virtual Type type() const = 0;
// Put any generic interfaces you want to have across all Value types here.
};
struct Integer: Value
{
int value;
Type type() const override { return Type::integer; }
};
struct String: Value
{
std::string value;
Type type() const override { return Type::str; }
};
struct Tuple: Value
{
std::vector<Value*> value;
Type type() const override { return Type::tuple; };
}
// etc. for whatever types are interesting to you.
Now you can define a function as anything that takes a single Value*
and returns a single Value*
. Multiple input or output arguments can be passed in as a Tuple, or a Map:
using Function = Value* (*)(Value*);
All your function implementations will need to get the type and do something appropriate with the argument:
Value* increment(Value* x)
{
switch (x->type())
{
Type::integer:
return new Integer(((Integer*) x)->value + 1);
Type::real:
return new Real(((Real*) x)->value + 1.0);
default:
throw TypeError("expected an integer or real argument.")
}
}
increment
is now compatible with the Function
type and can be stored in mFuncs
. You can now call a function of unknown type on arguments of unknown type and you will get an exception if the arguments don't match, or a result of some unknown type if the arguments are compatible.
Most probably you will want to store the function signature as something you can introspect, i.e. dynamically figure out the number and type of arguments that a Function
takes. In this case you can make a base Function
class with the necessary introspection functions and provide it an operator ()
to make it look something like calling a regular function. Then you would derive and implement Function
as needed.
This is a sketch, but hopefully contains enough pointers to show the way. There are also more type-safe ways to write this code (I like C-style casts when I've already checked the type, but some people might insist you should use dynamic_cast
instead), but I figured that is not the point of this question. You will also have to figure out how Value* objects lifetime is managed and that is an entirely different discussion.
c
with lots of different kinds of functions. How are you going to call them? How are you going to process function results for the non-void
functions? – Essive