Cancel any possible implicit conversion by making constructors explicit
.
Added Named_Assert_Type
type. See named_assert_example.cpp
section.
Added CMake constructor
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.
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.
Updated: Fri 01 Nov 2019 08:53:46 AM CET
For convenience, I just have included a CMake build solution that should work.
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.
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 ourOptionalArgument
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
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
You will find associated code in the examples/
directory.
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));
}
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");
}
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
}
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);
}
-> your questions here :-)