Search code examples
c++variadic-templatesgradient-descent

C++ variadic templates pass modified arguments to function


I'm struggling with the following code. It's meant to be a very simple, hopefully constexpr, gradient-descent solver.

My current code looks like this:

typedef std::function<double(double, double, double)> residual_function_t;

double gradient_descent(const residual_function_t& res_fun, double step_size, double& x, double& y, double& z,
                        double epsilon = 0.01) {
  assert(step_size > 0.0);

  const auto residual = res_fun(x, y, z);

  const auto deriv_x = (res_fun(x + epsilon, y, z) - residual) / epsilon;
  const auto deriv_y = (res_fun(x, y + epsilon, z) - residual) / epsilon;
  const auto deriv_z = (res_fun(x, y, z + epsilon) - residual) / epsilon;

  x -= std::copysign(step_size, deriv_x);
  y -= std::copysign(step_size, deriv_y);
  z -= std::copysign(step_size, deriv_z);

  return residual;
}

I'd like to make the function more general, and accept any kind of function. That means I have to create variables like deriv_x for each of the arguments I want to pass to that function.

I'd like my function signature to be something like this:

template<typename residual_function_t, typename... Arg_types>
double gradient_descent(const residual_function_t & res_fun, double step_size, Arg_types... arguments) {
constexpr auto epsilon = 0.01;

const auto residual res_fun(arguments...);

// Here I want to calculate deriv_x/y/z/etc for every argument passed.

// Here want to assign x/y/z, or return them if needed, for every argument passed.

return residual;
}

I wonder if this is possible? If it makes life easier to assume all arguments are of type double, that would also work.


Solution

  • If you want all the arguments to be of type double, then you might consider using std::initializer_list as follows:

    double gradient_descent(const residual_function_t& res_fun,
                            double step_size,
                            std::initializer_list<double> args,
                            double epsilon = 0.01) {
        // ...
    

    The downside is that you have to add an extra set of braces around the args when calling the function. Otherwise, while not strictly required, you may want to move epsilon before the variadic argument list to simplify your life. You have many options for handling a template parameter pack. One simple thing you can do in C++17 or later is to put the arguments into an array:

    double gradient_descent(const residual_function_t& res_fun,
                            double step_size,
                            double epsilon,
                            auto ...argspack) {
        std::array<double, sizeof...(argspack)> args(double(argspack)...);
        // ...
    }
    

    If you want to modify the arguments, that's fine, too, though they have to be doubles at that point:

    double gradient_descent(const residual_function_t& res_fun,
                            double step_size,
                            double epsilon,
                            std::same_as<double> auto &...argspack) {
        std::array<std::reference_wrapper<double>, sizeof...(argspack)> args(std::as_ref(argspack)...);
        // ...
    }