c++templatesfunction-pointerssquirrel

how to get the compiler to put a pointer to a function in a template without changing the signature of the template


I am supplementing squirrel scripting language and I need to make person easily register his own regular function to language. In order for the language to be able to call it, it must be generalized to SQFUNCTION (typedef SqInteger (*SQFUNCTION)(HSQUIRRELVM)).

I created a template that receives information about this function and receives arguments, but I need to somehow pass there a pointer to the function that the person wrote. if I make the second parameter of the template, then the compiler will not be able to convert this template to SQFUNCTION. I have no idea how to implement this.

template <typename T>
T GetArg(HSQUIRRELVM v, SQInteger idx);
 
template <>
inline int GetArg<int>(HSQUIRRELVM v, SQInteger idx) {
    SQInteger val;
    if (SQ_FAILED(sq_getinteger(v, idx, &val))) {
        throw sq_throwerror(v, "Failed to get integer");
    }
    return static_cast<int>(val);
}
 
template <typename Tuple, std::size_t... I>
Tuple GetArgsTuple(HSQUIRRELVM v, std::index_sequence<I...>) {
    return std::make_tuple(GetArg<std::tuple_element_t<I, Tuple>>(v, I + 2)...);
}
 
template <typename ReturnType>
void PushReturnValue(HSQUIRRELVM v, ReturnType value);
 
template <>
inline void PushReturnValue<int>(HSQUIRRELVM v, int value) {
    sq_pushinteger(v, static_cast<SQInteger>(value));
}
 
template <typename T>
struct function_traits;
 
template <typename Ret, typename... Args>
struct function_traits<Ret(*)(Args...)> {
    using return_type = Ret;
    using argument_types = std::tuple<Args...>;
    static constexpr std::size_t arg_count = sizeof...(Args);
};
 
template <typename Callable, typename Tuple, std::size_t... I>
auto CallFunc(Callable&& func, Tuple&& t, std::index_sequence<I...>) {
    return func(std::get<I>(std::forward<Tuple>(t))...);
}
 
template <typename FuncSignature>
SQInteger SqFuncWrapper(HSQUIRRELVM v) {
    using traits = function_traits<FuncSignature>;
    using ArgTuple = typename traits::argument_types;
    using RetType = typename traits::return_type;
    constexpr std::size_t arg_count = traits::arg_count;
 
    ArgTuple args = GetArgsTuple<ArgTuple>(v, std::make_index_sequence<arg_count>{});
    try {
        if (std::is_same_v<typename RetType,void>) {
            CallFunc(..., args, std::make_index_sequence<arg_count>{});
        }
        else {
            RetType ret = CallFunc(..., args, std::make_index_sequence<arg_count>{});
            PushReturnValue(v, ret);
        }
    }
    catch (const std::exception &e)
    {
        return sq_throwerror(v, e.what());
    }
 
    return 1;
}

Solution

  • First, I think the signature ...

    template <typename FuncSignature>
    SQInteger SqFuncWrapper(HSQUIRRELVM v)
    

    ... is a mistake since it'd require you to instantiate the function in order to be able to call it. Only lambdas / class-types would work in that case. Not plain functions.

    I'd choose to use a non-type argument for the function to be able to supply the function directly as a template parameter:

    template <auto Func>
    SQInteger SqFuncWrapper(HSQUIRRELVM v)
    

    or supply the function to call as an argument to SqFuncWrapper, which is probably clearer:

    template <typename FuncSignature>
    SQInteger SqFuncWrapper(HSQUIRRELVM v, FuncSignature&& func)
    

    Then in SqFuncWrapper, if (std::is_same_v<typename RetType,void>) should remove typename and preferably use constexpr-if:

    if constexpr (std::is_same_v<RetType, void>)
    

    You can also remove CallFunc. C++ has std::apply to do what CallFunc does.

    Example:

    template <class FuncSignature>
    SQInteger SqFuncWrapper(HSQUIRRELVM v, FuncSignature&& func) {
        using traits = function_traits<FuncSignature>;
        using ArgTuple = typename traits::argument_types;
        using RetType = typename traits::return_type;
        constexpr std::size_t arg_count = traits::arg_count;
    
        ArgTuple args =
            GetArgsTuple<ArgTuple>(v, std::make_index_sequence<arg_count>{});
        try {
            if constexpr (std::is_same_v<RetType, void>) {
                std::apply(std::forward<FuncSignature>(func), args);
            } else {
                RetType ret = std::apply(std::forward<FuncSignature>(func), args);
                PushReturnValue(v, ret);
            }
        } catch (const std::exception& e) {
            return sq_throwerror(v, e.what()); // suspicious return of exception
        }
        return 1;
    }
    

    Example calling SqFuncWrapper with a lambda taking an int and returning the same int:

    int main() {
        SqFuncWrapper(1, +[](int x) {
            std::cout << "Hello " << x << '\n';;
            return x;
        });
    }
    

    Now, note the + on the lambda. That's to make a regular function pointer out of it that will work with your function_traits class. It would not be able to accept a normal lambda, a capturing lambda or a user defined functor.


    You could drop in a replacement function_traits class that manages a some more variations. It's most probably not complete but it handles lambdas, regular functions, std::functions and even member functions (which is of no use here since you have no instance to call them on):

    template <class T>
    struct function_traits {
        template <class>
        struct function_traits_inner;
    
        template <class Ret, class... Args>
        struct function_traits_inner<Ret(Args...)> {
            template <class F>
            function_traits_inner(F&&) {}
            
            using return_type = Ret;
            using argument_types = std::tuple<Args...>;
            static constexpr std::size_t arg_count = sizeof...(Args);
        };
        //-------------------------------------------------------------------------
        template <class>
        struct function_guide_helper {};
    
        template <class Res, class Tp, bool Nx, class... Args>
        struct function_guide_helper<Res (Tp::*)(Args...) noexcept(Nx)> {
            using type = Res(Args...);
        };
    
        template <class Res, class Tp, bool Nx, class... Args>
        struct function_guide_helper<Res (Tp::*)(Args...) & noexcept(Nx)> {
            using type = Res(Args...);
        };
    
        template <class Res, class Tp, bool Nx, class... Args>
        struct function_guide_helper<Res (Tp::*)(Args...) const noexcept(Nx)> {
            using type = Res(Args...);
        };
    
        template <class Res, class Tp, bool Nx, class... Args>
        struct function_guide_helper<Res (Tp::*)(Args...) const & noexcept(Nx)> {
            using type = Res(Args...);
        };
    
        template <typename Fn, typename Op>
        using function_guide_t = typename function_guide_helper<Op>::type;
    
        // deduction guides:
        template <class Res, class... ArgTypes>
        function_traits_inner(Res (*)(ArgTypes...))
            -> function_traits_inner<Res(ArgTypes...)>;
    
        template <class Fn,
                  class Sig = function_guide_t<Fn, decltype(&Fn::operator())>>
        function_traits_inner(Fn) -> function_traits_inner<Sig>;
        //---------------------------------------------------------------------
        using trait = decltype(function_traits_inner(std::declval<T>()));
        using argument_types = typename trait::argument_types;
        using return_type = typename trait::return_type;
        static constexpr std::size_t arg_count = trait::arg_count;
    };
    

    Disclaimer: I know nothing about so I haven't been able to test it. I made a few functions emulating the squirrel functions to try it out though.

    Demo


    If you can't change the signature of SqFuncWrapper, you could go for the non-type template parameter version instead:

    template <auto func>
    SQInteger SqFuncWrapper(HSQUIRRELVM v) {
        using traits = function_traits<decltype(func)>;
        using ArgTuple = typename traits::argument_types;
        using RetType = typename traits::return_type;
        constexpr std::size_t arg_count = traits::arg_count;
    
        ArgTuple args =
            GetArgsTuple<ArgTuple>(v, std::make_index_sequence<arg_count>{});
        try {
            if constexpr (std::is_same_v<RetType, void>) {
                std::apply(func, args);
            } else {
                RetType ret = std::apply(func, args);
                PushReturnValue(v, ret);
            }
        } catch (const std::exception& e) {
            return sq_throwerror(v, e.what()); // suspicious return of exception
        }
        return 1;
    }
    

    Wrapping a function taking two ints and returning the sum could then be done with:

    SqFuncWrapper<[](int x, int y) { return x+y; }>
    

    Demo


    If you want to stick with your original type template parameter, you could reuse most of the function, but you'd then need to instantiate the function in order to pass it to std::apply:

    template <class FuncSignature>
    SQInteger SqFuncWrapper(HSQUIRRELVM v) {
        using traits = function_traits<FuncSignature>;
        using ArgTuple = typename traits::argument_types;
        using RetType = typename traits::return_type;
        constexpr std::size_t arg_count = traits::arg_count;
    
        ArgTuple args =
            GetArgsTuple<ArgTuple>(v, std::make_index_sequence<arg_count>{});
        try {
            if constexpr (std::is_same_v<RetType, void>) {
                std::apply(FuncSignature{}, args);
            } else {
                RetType ret = std::apply(FuncSignature{}, args);
                PushReturnValue(v, ret);
            }
        } catch (const std::exception& e) {
            return sq_throwerror(v, e.what()); // suspicious return of exception
        }
        return 1;
    }
    

    Demo