Search code examples
c++functiontypesfunction-pointers

Storing a pointer to a function with any return type and any number or arguments


I'm writing a custom JIT (interpreted) programming language called Trunk, and I'm trying to parse functions from code and store them in a global name -> function map, for now. But I'm stuck figuring out what type to use for the second template argument of std::map. Here is what I have so far:

namespace trunk {
    template<typename Ret, typename... Args>
    class Function {
        using FunctionType = Ret (*)(Args...);
        
        public:
            Function(FunctionType callable): _callable(callable) {}
            ~Function() {}
            
            Ret operator(Args... args) { 
                return _callable(args...);
            }
        
        protected:
            FunctionType _callable;
    };

    namespace globals {
        void print(std::string s) {
            std::cout << s << std::endl;
        }
    }
}

static trunk::Function print_function = trunk::Function(&trunk::globals::print);
static std::map< std::string, trunk::Function > function_map = {
    {"print", &print_function},
};

But that throws the following errors:

In file included from /media/frederic/WD-5TB/.fg/Programme/trunk/src/main.cxx:5:
/media/frederic/WD-5TB/.fg/Programme/trunk/include/trunk/function.hxx:15:37: error: expected type-specifier before ‘(’ token
   15 |                         Ret operator(Args... args) {
      |                                     ^
/media/frederic/WD-5TB/.fg/Programme/trunk/src/main.cxx:16:47: error: type/value mismatch at argument 2 in template parameter list for ‘template<class _Key, class _Tp, class _Compare, class _Alloc> class std::map’
   16 | static std::map< std::string, trunk::Function > function_map = {
      |                                               ^
/media/frederic/WD-5TB/.fg/Programme/trunk/src/main.cxx:16:47: note:   expected a type, got ‘Function’
/media/frederic/WD-5TB/.fg/Programme/trunk/src/main.cxx:16:47: error: template argument 4 is invalid
/media/frederic/WD-5TB/.fg/Programme/trunk/src/main.cxx:18:1: error: too many braces around scalar initializer for type ‘int’
   18 | };
      | ^

Solution

  • trunk::Function is not a type; it is a template: a blueprint from which any number of different types can be created. For example, trunk::Function<void, int> is a different type from trunk::Function<int, double>.

    Since there is no single trunk::Function type you can't create a std::map<std::string, trunk::Function>.

    If you want to have a single, heterogeneous map of all of your functions, you'll need to create some sort of type-erased wrapper around them and check the number and type of arguments and the type of return value at runtime.

    Here's a fairly basic example that does that:

    namespace trunk {
        class FunctionImplBase
        {
        public:
            virtual std::any call(const std::vector<std::any>& args) = 0;
        };
        
        template <typename Ret, typename... Args>
        class FunctionImpl : public FunctionImplBase
        {
        private:
            using FunctionType = Ret (*)(Args...);
            FunctionType callable_;
    
        public:
            FunctionImpl(FunctionType callable) : callable_{callable} {}
            
            std::any call(const std::vector<std::any>& args) override
            {
                if (args.size() != sizeof...(Args)) {
                    throw std::runtime_error("Wrong number of arguments supplied");
                }
                
                return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
                    if constexpr (std::is_same_v<Ret, void>) {
                        callable_(std::any_cast<Args>(args[Is])...);
                        return std::any{};
                    } else {
                        return callable_(std::any_cast<Args>(args[Is])...);
                    }
                }(std::make_index_sequence<sizeof...(Args)>{});
            }
        };
        
        class Function
        {
            public:
                Function() {}
            
                template <typename Ret, typename... Args>
                Function(Ret(*callable)(Args...))
                    : impl_{std::make_shared<FunctionImpl<Ret, Args...>>(callable)}
                {}
                
                std::any operator()(const std::vector<std::any>& args) const {
                    return impl_->call(args);
                }
            
            private:
                std::shared_ptr<FunctionImplBase> impl_;
        };
    }
    

    Demo

    The basic idea is that Function accepts any number of any type of argument, and then uses a polymorphic, type-erased FunctionImpl instantiation to check if it was given the number and type of arguments that the function it holds expects.

    Note that this implementation has some shortcomings. Namely around reference parameters. It's intended to show a possible approach you could take, more than a complete solution.