/OptionalArgument

Named Optional Arguments in C++17

Primary LanguageC++MIT LicenseMIT

Optional Argument in C++

News

Mon 02 Dec 2019 [0.0.2 tag]

Cancel any possible implicit conversion by making constructors explicit.

Thu 21 Nov 2019 [0.0.1 tag]

Added Named_Assert_Type type. See named_assert_example.cpp section.

Fri 01 Nov 2019

Added CMake constructor

What is it?

This is a C++17 one header file library that will allow you to define named optional arguments.

It can be used to improves code readability by replacing obscure function/method calls like

algorithm(x_init, 100, std::vector<double>(n, 0), std::vector<double>(n, 1), 1e-6, 1e-6)

By something like

algorithm(x_init, max_iterations = 100, absolute_precision = 1e-6);
algorithm(x_init, absolute_precision = 1e-6, lower_bounds<double> = std::vector<double>(n, 0));
algorithm(x_init);

supporting all the variations in position/definition for the optional arguments.

The required boilerplate code to add optional argument support is relatively light:

template <typename T, typename... USER_OPTIONS>
void algorithm(std::vector<T>& x, const USER_OPTIONS&... user_options)
{
  Absolute_Precision absolute_precision{1e-10};
  Max_Iterations max_iterations{500};
  std::optional<Allows_Restart> allows_restart;

  auto options = take_optional_argument_ref(absolute_precision, max_iterations, allows_restart);
  optional_argument(options, user_options...);

  // ...
}

Basically it works by defining a std::tuple<OPTIONS...> which is filled by the provided user_options at the site call.

Installation

The library currently uses the meson build system. Maybe I will add CMake in the future, but as this library is only one header file you can easily test it without any build system.

If you are not familiar with meson, the compilation procedure is as follows:

git clone git@github.com:vincent-picaud/OptionalArgument.git
cd OptionalArgument/
meson build
cd build
ninja test

Examples can then be found in the examples/ directory.

CMake

Updated: Fri 01 Nov 2019 08:53:46 AM CET

For convenience, I just have included a CMake build solution that should work.

Tutorial

Basic usage

The most “rustic” usage is:

#include <iostream>
#include "OptionalArgument/optional_argument.hpp"

using namespace OptionalArgument;

template <typename... Ts>
void example(Ts... user_options)
{
  int maximum_iterations{50};
  double absolute_precision{1e-5};
  std::optional<bool> allows_restart;

  auto options = take_optional_argument_ref(maximum_iterations, absolute_precision, allows_restart);
  optional_argument(options, user_options...);

  std::cout << "\nOption values: " << options;
}

int main()
{
  example(10);
  example(1e-6, std::optional<bool>(true));
  example();

  return EXIT_SUCCESS;
}
Option values: 10 1e-05 
Option values: 50 1e-06 1 
Option values: 50 1e-05

Basically we define some optional arguments like absolute_precision. Using a helper function take_optional_argument_ref() we collect their references and store them in the options object. Then optional argument values are overwritten by user_options provided at the call site. This task is performed by the optional_argument() function.

Named_Type

Preamble: great blog posts about Named_Typed can be found here. This was a source of inspiration but our implementation is different and simpler as it only provides functionalities useful for our OptionalArgument library.

There are several limitations if we only stick to types like int, double, std::string ... :

  • we cannot define several options of the same type,
  • we cannot relies on implicit type conversion.

By example, if you try:

#include "OptionalArgument/optional_argument.hpp"
#include <iostream>

using namespace OptionalArgument;

template <typename... Ts>
void example(Ts... user_options)
{
  size_t maximum_iterations{50};   // <- size_t instead of int
  float absolute_precision{1e-5};  // <- float instead of double

  auto options = take_optional_argument_ref(maximum_iterations, absolute_precision);
  optional_argument(options, user_options...);

  std::cout << "\nOption values: " << options;
}

int main()
{
  example(10);    // does not work, one would have to use: example(size_t(10));
  example(1e-6);  // does not work, one would have to use: example(1e-6f);

  return EXIT_SUCCESS;
}

the library does not compile and returns an error message:

...
OptionalArgument/optional_argument.hpp:122:46: error: static assertion failed: Unexpected type
static_assert((occurence_count == 1) || (occurence_count_maybe_optional == 1), "Unexpected type");
...

The solution is to use implicit type conversion and a different type for each option.

This library provides a basic Named_Type class for that. Please, note that you can use this library with any of your own class, it is by no way mandatory to use the provided Named_Type.

Using Named_Type the problematic initial code is fixed/rewritten as follows:

#include "OptionalArgument/optional_argument.hpp"

#include <iostream>

using namespace OptionalArgument;

using Absolute_Precision = Named_Type<struct Absolute_Precision_Tag, double>;
constexpr auto absolute_precision = typename Absolute_Precision::argument_syntactic_sugar();

using Maximum_Iterations = Named_Type<struct Maximum_Iterations_Tag, size_t>;
constexpr auto maximum_iterations = typename Maximum_Iterations::argument_syntactic_sugar();

template <typename... Ts>
void example(Ts... user_options)
{
  Maximum_Iterations maximum_iterations{50};
  Absolute_Precision absolute_precision{1e-5};

  auto options = take_optional_argument_ref(maximum_iterations, absolute_precision);
  optional_argument(options, user_options...);

  std::cout << "\nOption values: " << options;
}

int main()
{
  example(maximum_iterations = 10);
  example(absolute_precision = 1e-6);

  return EXIT_SUCCESS;
}
Option values: 10 1e-05 
Option values: 50 1e-06

Without Named_Type

You can use this library without any reference to the Named_Type class (which is only provided for convenience).

The example below shows how you can define optional arguments from scratch and use them.

#include "OptionalArgument/optional_argument.hpp"

#include <iostream>
#include <random>

using namespace OptionalArgument;

struct Sample_Size
{
  size_t n;

  size_t
  value() const
  {
    return n;
  }
};

struct Truncated
{
};

static constexpr auto sample_size = Argument_Syntactic_Sugar<Sample_Size, size_t>();
static constexpr auto truncated   = Truncated();

template <typename... USER_OPTIONS>
void
generate_sample(USER_OPTIONS&&... user_options)
{
  //// Options ////
  //
  Sample_Size sample_size{10};
  std::optional<Truncated> truncated;

  auto options = take_optional_argument_ref(sample_size, truncated);
  optional_argument(options, std::forward<USER_OPTIONS>(user_options)...);

  //// Implementation ////
  //
  std::random_device rd{};
  std::mt19937 gen{rd()};

  std::normal_distribution<> d{0, 1};

  for (size_t i = 0; i < sample_size.value(); i++)
  {
    auto sample = d(gen);
    if (truncated.has_value())
    {
      sample = std::abs(sample);
    }
    std::cout << sample << std::endl;
  }
  std::cout << std::endl;
}

int
main()
{
  generate_sample();

  generate_sample(sample_size = 5);

  generate_sample(truncated, sample_size = 5);
}
1.45544
-0.642569
0.377425
0.276248
1.48404
0.938607
0.575446
-1.49081
1.50139
0.142015

-0.664602
-0.184922
-0.415816
1.09387
-0.0196457

0.348479
1.76989
0.558797
0.835355
1.31337

More examples

You will find associated code in the examples/ directory.

algorithm_usage_example.cpp

An hypothetical optimization algorithm with several options, with some using the std::vector class.

int
main()
{
  const size_t n = 4;
  std::vector<double> x_init(n);

  // Option values: 100 1e-10 1e-10
  optimization_algorithm(x_init);

  // Option values: 50 1e-10 1e-10 0 0 0 0
  optimization_algorithm(x_init, max_iterations = 50,
                         lower_bounds<double> = std::vector<double>(n, 0));

  // Option values: 50 1e-08 1e-10 0 0 0 0  1 1 1 1
  optimization_algorithm(x_init, max_iterations = 50, absolute_precision = 1e-8,
                         lower_bounds<double> = std::vector<double>(n, 0),
                         upper_bounds<double> = std::vector<double>(n, 1));
}

plot_usage_example.cpp

Another use case that shows how one can easily generate gnuplot script commands with all their variations.

int
main()
{
  // prints:
  // plot sin(x)  linetype 2 title "my curve 1"
  // replot cos(x) linewidth 4 title "my curve 2"
  //
  plot(std::cout, "sin(x)", line_type = 2, curve_title = "my curve 1");
  replot(std::cout, "cos(x)", line_width = 4, curve_title = "my curve 2");
}

named_assert_example.cpp

The Named_Assert_Type type allows to control parameter value. A usage example is as follows:

#include "OptionalArgument/optional_argument.hpp"

#include <cassert>

using namespace OptionalArgument;

template <typename T>
struct Assert_Positive
{
  void
  operator()(const T& t) const
  {
    assert(t > 0);
  }
};

using Absolute_Precision =
    Named_Assert_Type<struct Absolute_Precision_Tag, Assert_Positive<double>, double>;
constexpr auto absolute_precision = typename Absolute_Precision::argument_syntactic_sugar();

void
my_algorithm(const Absolute_Precision& absolute_precision)
{
}

int
main()
{
  my_algorithm(absolute_precision = +1e-6);
  my_algorithm(absolute_precision = -1e-6);  // run-time assert fails
}

named_std_fonction.cpp

This allows to wrap std function.

Note: we cannot directly wrap λ as we need some type erasure to use optional argument.

#include "OptionalArgument/optional_argument.hpp"

#include <cassert>
#include <valarray>

using namespace OptionalArgument;

using Objective_Function =
    Named_Std_Function<struct Objective_Function_Tag, double, const std::valarray<double>&>;
constexpr auto objective_function = Argument_Syntactic_Sugar<Objective_Function>();

void
my_algorithm(const Objective_Function& obj_f, std::valarray<double>& x_init)
{
  std::cout << "Value = " << obj_f(x_init) << std::endl;
}

double
Rosenbrock(const std::valarray<double>& x, double c)
{
  assert(x.size() == 2);

  return (1 - x[0]) * (1 - x[0]) + c * (x[1] - x[0] * x[0]) * (x[1] - x[0] * x[0]);
}

double
Rosenbrock(const std::valarray<double>& x)
{
  return Rosenbrock(x, 10);
}

template <typename T>
struct Rosenbrock_as_Struct
{
  double c = 200;

  T
  operator()(const std::valarray<T>& x) const
  {
    assert(x.size() == 2);

    return (1 - x[0]) * (1 - x[0]) + c * (x[1] - x[0] * x[0]) * (x[1] - x[0] * x[0]);
  }
};

int
main()
{
  std::valarray<double> x(2);
  x = -1;

  my_algorithm(objective_function = Rosenbrock, x);

  my_algorithm(
      objective_function = [](const std::valarray<double>& x) { return Rosenbrock(x, 100); }, x);

  my_algorithm(objective_function = Rosenbrock_as_Struct<double>(), x);

  Rosenbrock_as_Struct<double> f;
  my_algorithm(objective_function = f, x);
}

FAQ

-> your questions here :-)