c++c++20std-functionstdany

Like std::function but with more varied argument and return types


I'm in search of a way to set up and call functions with arbitrary arguments and return types. One use case would be high level scripting. Something like this:

// universal function
using dynfunction = std::any (*)(std::vector<std::any> args);

I made a simplified example:

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

using namespace std::string_literals;
using namespace std::string_view_literals;

class MyClass {
public:
    int foo(double d, std::string m) {
        std::cout << "foo " << d << ", " << m << std::endl;
        return 42;
    }

    virtual double bar(int i) {
        return -i;
    }
};

class MyDerivedClass : public MyClass {
    virtual double bar(int i) override {
        return -i*3.1412;
    }
};

void foobar(char c) {
    std::cout << "foobar " << c << std::endl;
}

// universal function
using dynfunction = std::any (*)(std::vector<std::any> args);

// caller wrappers
std::any call_foo(std::vector<std::any> args) {
    return std::any_cast<MyClass*>(args[0])->foo(std::any_cast<double>(args[1]), std::any_cast<std::string>(args[2]));
}

std::any call_bar(std::vector<std::any> args) {
    return std::any_cast<MyClass*>(args[0])->bar(std::any_cast<int>(args[1]));
}

std::any call_foobar(std::vector<std::any> args) {
    foobar(std::any_cast<char>(args[0]));
    return {}; // void
}

// demonstrate dynamic resolution
std::map<const std::string_view, const dynfunction> functions = {
        { "foo"sv, call_foo },
        { "bar"sv, call_bar },
        { "foobar"sv, call_foobar }
};

int main() {
    MyClass obj;
    std::any ret = functions["foo"sv](std::vector<std::any>{&obj, 7.0, "Hello World!"s});
    ret = functions["bar"sv](std::vector<std::any>{&obj, ret});
    std::cout << "obj.bar returned " << std::any_cast<double>(ret) << std::endl;
    MyDerivedClass obj2;
    ret = functions["bar"sv](std::vector<std::any>{dynamic_cast<MyClass*>(&obj2), 11}); // derived class must be cast to base
    std::cout << "obj2.bar returned " << std::any_cast<double>(ret) << std::endl;
    functions["foobar"sv](std::vector<std::any>{'x'});
}

https://godbolt.org/z/rMcTo9

This works, but I wonder if there are simpler or more direct ways.

Also, one thing that bothers me is the need to cast polymorphic types i.e. objects of derived classes to the right base class for this to work (see obj2 in the example). Is there a way around this?

Note: the example is deliberately simplified (use of std::vector for arguments, std::map for lookup, these are just conceptual stand-ins).

Edit: It seems more information is needed.

This is part of a "Model-driven architecture" (MDA) (not the OMG variant - our solution predates that by ~6 years). We have our own OOP/4GL language "V" we created in 1995. It retains all meta information at runtime. This allows us to generate all GUI, database design, data binding, Scripting interface you-name-it dynamically. In other languages this is nowadays called "Reflection", but what is available in Java&Co is quite limited in comparison to what we can do.

The MDA means (among many other things) that we need a way to transfer control from the dynamic "model driven" part of the solution to the actual application logic i.e. the functions. The built-in scripting language we have is just one of many use cases, and I thought it is the easiest to understand for a general audience.

The amount of primitive data types is limited, while there are thousands of polymorphic data types. std::any seemed more elegant than std::variant because a) it has this cool but completely transparent heap optimization, (i.e. small data types need no dynamic allocation) and b) we can keep separation of concerns intact between the data types i.e. all the std::any handling code can remain unchanged if we add a new data type. But if there are important advantages for std::variant, I'm open to consider it.


Solution

  • Here is my design:

    #include <any>
    #include <cstddef>
    #include <cstdio>
    #include <functional>
    #include <stdexcept>
    #include <tuple>
    #include <type_traits>
    #include <utility>
    #include <vector>
    
    class Dyn_fun final {
     public:
      using arg_type = std::vector<std::any>;
      using return_type = std::any;
    
      Dyn_fun() = default;
    
      template <typename Ret, typename... Args>
      explicit Dyn_fun(Ret (*fp)(Args...)) : m_dispatcher{make_dispatcher(fp)} {}
    
      template <typename Ret, typename... Args>
      Dyn_fun& operator=(Ret (*fp)(Args...)) {
        set_function(fp);
        return *this;
      }
    
      template <typename Ret, typename... Args>
      void set_function(Ret (*fp)(Args...)) {
        m_dispatcher = make_dispatcher(fp);
      }
    
      explicit operator bool() const noexcept {
        return static_cast<bool>(m_dispatcher);
      }
      bool operator==(std::nullptr_t) const noexcept {
        return m_dispatcher == nullptr;
      }
      bool operator!=(std::nullptr_t) const noexcept { return !(*this == nullptr); }
    
      return_type operator()(arg_type const& args) { return m_dispatcher(args); }
    
     private:
      using dispatcher_type = std::function<return_type(arg_type const&)>;
    
      template <typename Ret, typename... Args>
      static auto make_dispatcher(Ret (*fp)(Args...)) {
        return make_dispatcher(fp, std::make_index_sequence<sizeof...(Args)>{});
      }
    
      // Extracts type at a specific index of argument pack
      template <size_t index, typename... Args>
      using type_at = std::decay_t<decltype(std::get<index>(
          std::declval<std::tuple<Args...>>()))>;
    
      template <typename Ret, typename... Args, size_t... Indices>
      static auto make_dispatcher(Ret (*fun)(Args...),
                                  std::index_sequence<Indices...>) {
        return [fun](arg_type const& args) {
          if (args.size() != sizeof...(Args))
            throw std::runtime_error{"Argument count does not match"};
    
          // this avoids std::any{ void }
          if constexpr (!std::is_void_v<Ret>) {
            return std::any(
                fun(std::any_cast<type_at<Indices, Args...>>(args[Indices])...));
          } else {
            fun(std::any_cast<type_at<Indices, Args...>>(args[Indices])...);
            return std::any{};
          }
        };
      }
    
      dispatcher_type m_dispatcher;
    };
    
    int foo(int i, char const* s, double d) {
      return std::printf("foo: %d, %s, %.2f\n", i, s, d);
    }
    
    void print_sum(int a, int b) {
      std::printf("print_sum: %d + %d = %d\n", a, b, a + b);
    }
    
    int main() {
      // Construct with foo
      Dyn_fun dyn_fun(foo);
    
      Dyn_fun::arg_type args{10, "Hello", 4.2};
    
      // Call with arguments
      dyn_fun(args);
    
      // Change the wrapped function
      dyn_fun = print_sum;
    
      // with different arguments
      args.assign({10, 20});
    
      // Call the new function
      dyn_fun(args);
    }
    

    Highlights: