Search code examples
c++visual-c++variadic-templates

Derive (virtual) function arguments in variadic template class


I'm building an interpreter and trying to avoid some boilerplate I run into when implementing builtin-functions. I am able to to do this by using templates.

Take this base template for instance:

template<ast::builtin_type T>
class builtin_procedure abstract : public builtin_procedure_symbol
{
    using arg_traits = builtin_type_traits<T>;

protected:
    builtin_procedure(const symbol_identifier& identifier): builtin_procedure_symbol(identifier)
    {
        this->register_param(arg_traits::param_id(), T);
    }

    /**
     * The actual implementation of the built-in function
     */
    virtual void invoke_impl(typename arg_traits::builtin_type) = 0;

public:
    void invoke(scope_context& procedure_scope) override
    {
        auto raw_arg = procedure_scope.memory->get(procedure_scope.symbols.get(arg_traits::param_id()));
        this->invoke_impl(arg_traits::get_from_expression(raw_arg));
    }
};

To implement a built-in function function that takes a string, I only need to do:

class builtin_procedure_writeln final : public builtin_procedure<ast::builtin_type::string>
{
protected:
    void invoke_impl(arg_traits::builtin_type arg) override;

public:
    builtin_procedure_writeln();
}; /* Implementation in cpp file */

Very convenient, I only need to implement the virtual invoke_impl method and that's it.

I'm trying to wrap my head around getting this implemented with a variable number of template arguments so I don't have to duplicate my template definition if I want to support 2, 3, or more arguments in my derived implementation like in the example below.

This would be the template above to support a second template parameter:

template<ast::builtin_type T1, ast::builtin_type T2>
class builtin_procedure abstract : public builtin_procedure_symbol
{
    using arg1_traits = builtin_type_traits<T1>;
    using arg2_traits = builtin_type_traits<T2>;

protected:
    builtin_procedure(const symbol_identifier& identifier): builtin_procedure_symbol(identifier)
    {
        this->register_param(arg_traits::param_id(1), T1);
        this->register_param(arg_traits::param_id(2), T2);
    }

    /**
     * The actual implementation of the built-in function
     */
    virtual void invoke_impl(typename arg1_traits::builtin_type, typename arg2_traits::builtin_type) = 0;

public:
    void invoke(scope_context& procedure_scope) override
    {
        auto raw_arg1 = procedure_scope.memory->get(procedure_scope.symbols.get(arg1_traits::param_id()));
        auto raw_arg2 = procedure_scope.memory->get(procedure_scope.symbols.get(arg2_traits::param_id()));
        this->invoke_impl(arg1_traits::get_from_expression(raw_arg1), arg2_traits::get_from_expression(raw_arg2));
    }
};

I know that essentially through template recursion you can essentially iterate through each of the template parameters to do whatever you want to do, but what about the definition of the virtual invoke_impl method? Each of the parameters are derived from the the traits struct, and the call to the method itself also seems not something you could some with template recursion.

How (if) it possible to use a variadic template to allow for a variable number of arguments on this base class as an alternative to just copy/paste this base class with more template arguments?


The final clue was given n314159, this works:

template<ast::builtin_type... Ts>
class builtin_procedure abstract : public builtin_procedure_symbol
{
private:
    template<ast::builtin_type T>
    typename builtin_type_traits<T>::builtin_type make_arg(scope_context& procedure_scope, int param_id)
    {
        auto raw_arg = procedure_scope.memory->get(procedure_scope.symbols.get(builtin_type_traits<T>::param_id(param_id++)));
        return builtin_type_traits<T>::get_from_expression(raw_arg);
    }

protected:
    builtin_procedure(const symbol_identifier& identifier, ::symbol_table* runtime_symbol_table): builtin_procedure_symbol(identifier, runtime_symbol_table)
    {
        auto param_id = 0;

        ((void) this->register_param(builtin_type_traits<Ts>::param_id(++param_id), Ts), ...);
    }

    virtual void invoke_impl(typename builtin_type_traits<Ts>::builtin_type...) = 0;

public:
    void invoke(scope_context& procedure_scope) override
    {
        auto param_id = 0;
        this->invoke_impl(make_arg<Ts>(procedure_scope, ++param_id)...);
    }
};

Solution

  • So, I wrote a small example. I don't think one can do aliasing for variadic templates, so I left that out, but it works without even if it is less nice. So, since I can't use non-integral non-type template parameters, I switched your ast::builtin_type to int, but I think you can reverse that easily enough. The following compiles (but doesn't link, obviously^^).

    template<int i>
    struct builtin_traits {
        static int param_id(int) { return i;}
        using builtin_type = int;
    };
    
    class builtin_procedure_symbol {
        void register_param(int, int);
    };
    
    int get(int); // my replacement for procedure_scope.memory->get(procedure_scope.symbols.get
    
    template<int... Ts>
    class builtin_procedure : builtin_procedure_symbol{
        builtin_procedure(): builtin_procedure_symbol()
        {
            ((void) this->register_param(builtin_traits<Ts>::param_id(1), Ts), ... );
        }
    
        virtual void invoke_impl(typename builtin_traits<Ts>::builtin_type...) = 0;
    
        void invoke() 
        {
            auto f = [&](const auto& arg) {
                auto raw_arg = get(builtin_traits<arg>::param_id());
                return builtin_traits<arg>::get_from_expression(raw_arg);
            };
            this->invoke_impl(f(Ts)...);
        }
    };
    

    I hope that helps you. If something is unclear, please ask.