c++c++17polymorphismvariadic-templatesvariadic-functions

How to create nested variadic functions?


I can't and won't bore you with the details, but my system has these specific requirements:

  1. Actions must be called and registered at runtime.
  2. Each Action can have multiple targets, and these targets should be processed in a loop.
  3. Actions may accept special arguments, but any potentially available action can be invoked through a generalized Call function at runtime.
  4. If incorrect argument count or types are passed to the Call function, it should return false instead of causing a runtime crash.

Conceptually, this is quite simple: We register an action, and then we call the action by its name and with arguments of given type. This would mean the call can be done from anywhere in code at runtime.

Smallest reproducible example: https://onlinegdb.com/qNErEq_85

Currently, an example Action looks like this:

bool DoAction(const int Target, ...)
{
    va_list args;
    va_start(args, Target); // Initialize the va_list with the last named parameter

    // Assume the first argument after Targets is a std::string
    const char* stringArg = va_arg(args, const char*); // Get the string argument
    std::string myString(stringArg); // Convert it to std::string

    va_end(args); // Clean up the va_list

    std::cout << myString << " \n";

    return true;
}

This is how the Call() function looks like:

bool Call(const int Target, ...) override
{
    std::cout << "called";
    
    va_list args;
    va_start(args, Target);

    bool returnValue = (Instance->*ActionFunction)(Target, args);

    va_end(args);

    return returnValue;
}

The critical issue with this implementation is that va_list consumes the arguments, so they cannot be nested.

I have tried replacing virtual with a template, but the std::map<std::string, Action*> RegisteredActions; still requires derivation. dynamic_cast into the derived type is however not possible, because Call() should not take any templates or arguments other than the function name and function arguments (otherwise there would not be much point to using this system).

Solutions I have not tried:

  1. Hard-coding specific argument types. This should work, but then there wouldn't be much of a point to using this system.
  2. Make Call() a macro. This could work, but I haven't figured out how.
  3. Not doing this.

There are no questions about this in the site, it's mostly about asking about specific compilation errors. My question is how to implement a specific type of runtime functionality.

How do I create nested variadic functions?


Solution

  • You might get rid of C variadic and use types from C++:

    std::map<std::string, std::function<bool(int, std::vector<std::any>)>> RegisteredActions{
        { "FooAction", &FooAction},
        // ...
    };
    
    RegisteredActions["BarAction"] = &BarAction;
    

    Function looks like:

    bool FooAction(int target, std::vector<std::any> v) {
        if (v.size() != 1) { // check size
            return false;
        }
        if (auto* s = std::any_cast<std::string>(&v[0])) { // check type
            std::cout << target << ": " << *s << std::endl;
            return true;
        }
        return false;
    }
    

    Note: Care with type and type conversion: const char* is not std::string

    assert(FooAction(42, "const char*") == false);
    assert(FooAction(42, "std::string"s) == true);
    

    Call look like:

    int target = 42;
    std::vector<std::any> args = { "Hello"s };
    const auto it = RegisteredActions.find(Name);
    if (it == RegisteredActions.end())
    {
        // ERROR: No action of name has been registered
        throw std::runtime_error("Action not found");
    }
    auto res = it->second(target, args);
    

    Demo