c++c++14variadic-templates

c++ 14 Variadic Templates to call different methods with enum


I want to have 2 method, one is SetVariable and the other GetVariable in MyClass both will receive an enum and arguments, the arguments can be more then one and more then one type and according to the enum the relevant private method will be called I want to call those method from a different class called OtherClass (those are not the real names but for the explanation it is) I triad with ChatGPT and it told me about Variadic Templates but the problem is that I got wrong argument number error

template <typename... Args>
    bool SetVariable(const int& variable, Args&&... args)
    {
        auto returnValue = false;
        switch (variable)
        {
        case 1:
        {
            returnValue = Function1(std::forward<Args>(args)...);
            break;
        }
        case 2:
        {
            returnValue = Function2(std::forward<Args>(args)...);
            break;
        }

        default:
            break;
        }

        return returnValue;
    }

this is both function1 and 2:

Function1( const int & axis_, const double& velocity_ )
Function2( std::vector<int>& chameleonFirmwareVec_ )

this is how I call SetVariable from a different class

bool Core::OtherClass::Function1Call( const int & axis_, const double& velocity_ )
{
    return MyClass.SetVariable(1, axis_, velocity_);
}

the error here is Function2': function does not take 2 arguments and I dont event call it, if I have more then one case the error ampere


Solution

  • The problem is that here the enum value (the variable) is determined at runtime, whereas the type of the arguments (Args) is determined at compile time.

    So when calling SetVariable(1, axis_, velocity_), the compiler generates a template instantiation for a function SetVariable(int, double), and this function contains a call to Function2(int, double), which does not exist. And this causes the compile error.

    Two ways to avoid it would be to either make the argument types also runtime-determined, or make the enum compile-time determined.


    To make the type of the arguments runtime-determined, one way would be to make it like this:

    using AxisVelocity = std::pair<int, double>;
    void SetVariable(
       int variable,
       const std::variant<
           AxisVelocity,
           std::vector<int>
       >& value
    ) {
       if(variable == 1) {
          AxisVelocity axisVelocity = std::get<0>(value);
          Function1(axisVelocity.first, axisVelocity.second);
       } else if(variable == 2) {
          Function2(std::get<1>(value));
       }
    }
    
    SetVariable(1, AxisVelocity(axis, velocity));
    

    Like this it would throw an exception at runtime (from std::get) if the type does not match the argument.

    This could of course be further refined, for example by making a wrapper type around the std::variant, that also does implicit conversions between similar types, or having a varadic SetVariable(variable, Args...) that forwards to SetVariable(variable, std::tuple<Args...>), etc.


    Another solution is to make the enum variable also compile-time determined, for example:

    template<int Variable, class... Args>
    void SetVariable(Args&&... args) {
        if constexpr(Variable == 1) {
            Function1(args...);
        } else if constexpr(Variable == 2) {
            Function2(args...);
        }
    }
    
    SetVariable<1>(axis, velocity);
    

    The if constexpr makes sure that the branches that are not taken (depending on the compile-time Variable) are not compiled, and so it does not need Function2(int, double) to exist.

    But in this case it is maybe better to just have different functions for the different enums.


    Another way is to use SFINAE with a helper function like CallIfPossible, that calls the function if it can take these argments, or if not returns false:

    #include <vector>
    #include <utility>
    
    bool Function1(int axis, double velocity) { return true; }
    bool Function2(const std::vector<int>& data) { return true; }
    
    template<class Function, class... Args>
    auto CallIfPossible(const Function& fct, Args&&... args) -> decltype(fct(std::forward<Args>(args)...)) {
        // SFINAE: template is only instantiated if fct accepts these arguments
        return fct(std::forward<Args>(args)...);
    }
    template<class Function>
    bool CallIfPossible(const Function&, ...) {
        // fallback
        return false;
        // or maybe throw an exception
    }
    
    template<class... Args>
    bool SetVariable(int variable, Args&&... args) {
        switch(variable) {
        case 1:
            return CallIfPossible(Function1, std::forward<Args>(args)...);
        case 2:
            return CallIfPossible(Function2, std::forward<Args>(args)...);
        }
        return false;
    }
    
    int main() {
        int axis = 1;
        double velocity = 10.0;
        SetVariable(1, axis, velocity);
        
        std::vector<int> data;
        SetVariable(2, data);
    }
    

    This works from C++11 on. Inside of SetVariable, the compiler will still generate template instantiations for all the combinations. For example also CallIfPossible(Function2, int, double) for calling Function2 with int, double.

    But then during template substitution, decltype(Function2(axis, velocity)) (which would normally result in bool) cannot be evaluated, because there is no Function2 overload that would take (int, double) arguments. But this does not result in a compile error ("substitution failure is not an error").

    So the compiler instead uses the template specialization CallIfPossible(Function2, ...) which always returns false. And it never instantiates a template function which would call Function2(int, double).