6

I'm trying to generate an argument list for a function call during runtime, but I can't think of a way to accomplish this in c++.

This is for a helper library I'm writing. I'm taking input data from the client over a network and using that data to make a call to a function pointer that the user has set previously. The function takes a string(of tokens, akin to printf), and a varying amount of arguments. What I need is a way to add more arguments depending on what data has been received from the client.

I'm storing the functions in a map of function pointers

typedef void (*varying_args_fp)(string,...);
map<string,varying_args_fp> func_map;

An example usage would be

void printall(string tokens, ...)
{
    va_list a_list;
    va_start(a_list, tokens);

    for each(auto x in tokens)
    {
        if (x == 'i')
        {
            cout << "Int: " << va_arg(a_list, int) << ' ';
        }
        else if(x == 'c')
        {
            cout << "Char: " << va_arg(a_list, char) << ' ';
        }
    }

    va_end(a_list);
}

func_map["printall"] = printall;
func_map["printall"]("iic",5,10,'x');
// prints "Int: 5 Int: 10 Char: x"

This works nicely when hardcoding the function call and it's arguments, but if I've received the data "CreateX 10 20", the program needs to be able to make the argument call itself. eg

// func_name = "CreateX", tokens = 'ii', first_arg = 10, second_arg = 20
func_map[func_name](tokens,first_arg,second_arg);

I can't predict how users are going to lay out the functions and code this beforehand.

If anyone has suggestions on accomplishing this task another way, feel free to suggest. I need the user to be able to "bind" a function to the library, and for the library to call it later after it has received data from a networked client, a callback in essence.

9
  • 1
    Run-time varying number of arguments? Not possible in C++ AFAIK, or it would be very bad anyway. The (syntactical) problem is not to receive them, but rather to pass them. It should be possible in assembler, though. In C++, you'd rather use a data structure to store the arguments and pass this structure, like a std::list<string>. I'd suggest take a look at boost.spirit Commented Apr 2, 2013 at 12:36
  • This way of passing variable number of arguments is deprecated in C++ Commented Apr 2, 2013 at 12:44
  • That was my initial thought, but if a function takes 2 different types of arguments, then I can't store them together. Commented Apr 2, 2013 at 12:45
  • 1
    You can use boost::variant or boost::any if you want different types of arguments Commented Apr 2, 2013 at 12:47
  • 2
    Perhaps you're trying to fulfill Greenspun's 10th rule Commented Apr 2, 2013 at 12:57

2 Answers 2

8

Here is a C++11 solution. It does not support varargs functions like printall or printf, this is impossible with this technique and IMO impossible at all, or at the very least extremely tricky. Such function are difficult to use safely in an environment like yours anyway, since any bad request from any client could crash the server, with absolutely no recourse whatsoever. You probably should move to container-based interface for better safety and stability.

On the other hand, this method supports all (?) other functions uniformly.

#include <vector>
#include <iostream>
#include <functional>
#include <stdexcept>
#include <string>
#include <boost/any.hpp>


template <typename Ret, typename... Args>
Ret callfunc (std::function<Ret(Args...)> func, std::vector<boost::any> anyargs);

template <typename Ret>
Ret callfunc (std::function<Ret()> func, std::vector<boost::any> anyargs)
{
    if (anyargs.size() > 0)
        throw std::runtime_error("oops, argument list too long");
    return func();
}

template <typename Ret, typename Arg0, typename... Args>
Ret callfunc (std::function<Ret(Arg0, Args...)> func, std::vector<boost::any> anyargs)
{
    if (anyargs.size() == 0)
        throw std::runtime_error("oops, argument list too short");
    Arg0 arg0 = boost::any_cast<Arg0>(anyargs[0]);
    anyargs.erase(anyargs.begin());
    std::function<Ret(Args... args)> lambda =
        ([=](Args... args) -> Ret {
         return func(arg0, args...);
    });
    return callfunc (lambda, anyargs);
}

template <typename Ret, typename... Args>
std::function<boost::any(std::vector<boost::any>)> adaptfunc (Ret (*func)(Args...)) {
    std::function<Ret(Args...)> stdfunc = func;
    std::function<boost::any(std::vector<boost::any>)> result =
        ([=](std::vector<boost::any> anyargs) -> boost::any {
         return boost::any(callfunc(stdfunc, anyargs));
         });
    return result;
}

Basically you call adaptfunc(your_function), where your_function is a function of any type (except varargs). In return you get an std::function object that accepts a vector of boost::any and returns a boost::any. You put this object in your func_map, or do whatever else you want with them.

Types of the arguments and their number are checked at the time of actual call.

Functions returning void are not supported out of the box, because boost::any<void> is not supported. This can be dealt with easily by wrapping the return type in a simple template and specializing for void. I've left it out for clarity.

Here's a test driver:

int func1 (int a)
{
    std::cout << "func1(" << a << ") = ";
    return 33;
}

int func2 (double a, std::string b)
{
    std::cout << "func2(" << a << ",\"" << b << "\") = ";
    return 7;
}

int func3 (std::string a, double b)
{
    std::cout << "func3(" << a << ",\"" << b << "\") = ";
    return 7;
}

int func4 (int a, int b)
{
    std::cout << "func4(" << a << "," << b << ") = ";
    return a+b;
}


int main ()
{
    std::vector<std::function<boost::any(std::vector<boost::any>)>> fcs = {
        adaptfunc(func1), adaptfunc(func2), adaptfunc(func3), adaptfunc(func4) };

    std::vector<std::vector<boost::any>> args =
    {{777}, {66.6, std::string("yeah right")}, {std::string("whatever"), 0.123}, {3, 2}};

    // correct calls will succeed
    for (int i = 0; i < fcs.size(); ++i)
        std::cout << boost::any_cast<int>(fcs[i](args[i])) << std::endl;

    // incorrect calls will throw
    for (int i = 0; i < fcs.size(); ++i)
        try {
            std::cout << boost::any_cast<int>(fcs[i](args[fcs.size()-1-i])) << std::endl;
        } catch (std::exception& e) {
            std::cout << "Could not call, got exception: " << e.what() << std::endl;
        }
}
Sign up to request clarification or add additional context in comments.

Comments

2

As already mentioned by @TonyTheLion, you can use boost::variant or boost::any to select between types at runtime:

typedef std::function<void(const std::string&, const std::vector<boost::variant<char, int>>&)> varying_args_fn;
std::map<std::string, varying_args_fn> func_map;

The you can e.g. use a static visitor to distinguish between the types. Here is a full example, note that the tokens parameter is actually no longer necessary since boost::variant knows at runtime what type is stored in it:

#include <map>
#include <vector>
#include <string>
#include <functional>
#include <iostream>

#include <boost/variant.hpp>
#include <boost/any.hpp>

typedef std::function<void(const std::string&, const std::vector<boost::variant<char, int>>&)> varying_args_fn;

void printall(const std::string& tokens, const std::vector<boost::variant<char, int>>& args) {
  for (const auto& x : args) {
    struct : boost::static_visitor<> {
      void operator()(int i) {
        std::cout << "Int: " << i << ' ';
      }
      void operator()(char c) {
        std::cout << "Char: " << c << ' ';
      }
    } visitor;
    boost::apply_visitor(visitor, x);
  }
}

int main() {
  std::map<std::string, varying_args_fn> func_map;
  func_map["printall"] = printall;
  func_map["printall"]("iic", {5, 10, 'x'});
}

2 Comments

Any reason to use a vector instead of a list here?
@DyP No specific reason, list would work as well. However, vector is sometimes considered the "default" sequential container (although others say that should rather be deque), and often leads to better performance due to its sequential memory layout.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.