C++ Policies for Compile-Time Configuration

This posts implements a policy class that can be used to configure the behaviour of other classes, such as numerical algorithms, at compile time.

I have recently been working on a finite difference PDE solver that allows for different configuration options. Think for example about different grid construction algorithms, backward induction schemes and so on. As run-time polymorphism was not needed, I implemented the configuration through template parameters. An early draft looked something like this:

namespace grid
{
    struct EqualInPrices { };
    struct EqualInLogs { };
}

namespace induction
{
    struct SchemeA { };
    struct SchemeB { };
}

template<class GridConstruction = grid::EqualInPrices,
         class BackwardInduction = induction::SchemeA>
class FiniteDifferenceSolver
{
    ...
};

As I kept refining the PDE solver, more and more implementation choices arose and additional template parameters were added to represent them. While this approach worked, it was neither user friendly nor maintainable. Consider for example the situation, when we want to pass a non-default type only for the n-th template parameter. Then all prior ones also have to be explicitly specified, even though we would like them to stay at their default value. When reading the code, it is not obvious which of the first n template parameters were meant to be set to a custom type and which not. Furthermore, changes to the default values had to be applies throughout the code base. In an attempt to mitigate these issues, I frequently refactored the code, moving the template parameters that were often set to custom types further to the front and vice versa.

Ideally, the interface would require only the non-default configuration options to be passed. It would further allow us to do so without adhering to any specific order. This is very similar to how the policy classes used in the Boost Math library can be used to control among others, numeric precision and error handling. The new PDE solver interface then accept only a single template parameter corresponding to its policy class.

template<class Policy = FiniteDifferencePolicy<>>
class FiniteDifferenceSolver
{
    ...
};

Possible constructions include

// default policy
typedef FiniteDifferencePolicy<> DefaultPolicy;
FiniteDifferenceSolver<DefaultPolicy> solver;
// we only specify the grid construction
typedef FiniteDifferencePolicy<grid::EqualInLogs> CustomGridPolicy;
FiniteDifferenceSolver<CustomGridPolicy> solver;
// we only specify the induction scheme
typedef FiniteDifferencePolicy<induction::SchemeB> CustomInductionPolicy;
FiniteDifferenceSolver<CustomInductionPolicy> solver;
// we specify both - the order is irrelevant
typedef FiniteDifferencePolicy<grid::EqualInLogs, induction::SchemeB> FullyCustomPolicy;
FiniteDifferenceSolver<CustomInductionPolicy> solver;

The policy class itself is implemented as a variadic template with a number of type members corresponding to the different configuration options. For any group of options, e.g. grid construction algorithms, the corresponding unique type entry from the variadic parameter list is used. If no such entry exists, a default type is used. If multiple type entries from one group exist, a static assertion failure results in a compile time error.

Before defining FiniteDifferencePolicy<...>, we need to construct two auxiliary classes that check whether a type belongs to the namespaces grid and induction, respectively.

POLICY_VALIDATOR(GridValidator, grid::EqualInPrices, grid::EqualInLogs)
POLICY_VALIDATOR(InductionValidator, induction::SchemeA, induction::SchemeB)

The macro POLICY_VALIDATOR(...) constructs a template that has a static member value, indicating whether the template parameter type is in the variadic list passed to it.

template<class T, typename... Us>                                                                                                                                                                                                          struct PolicyValidatorBase
{
    static constexpr auto list = hana::tuple_t<Us...>;
    static const bool value = hana::contains(list, hana::type_c<T>);
    typedef std::integral_constant<bool, value> type;
};

#define POLICY_VALIDATOR(ValidatorName, ...)                           \
template<typename T>                                                   \
struct ValidatorName : public PolicyValidatorBase<T, __VA_ARGS__> { };

The type check is implemented using the Boost Hana meta-programming library, which we assume is included as follows

#include <boost/hana.hpp>
namespace hana = boost::hana;

We can now define the FiniteDifferencePolicy<...> through

template<typename... Ts>
struct FiniteDifferencePolicy
{
    POLICY_TYPE(GridConstruction, GridValidator, grid::EqualInPrices, Ts...)
    POLICY_TYPE(BackwardInduction, InductionValidator, induction::SchemeA, Ts...)
};

Again, most of the magic happens in a variadic macro

#define POLICY_TYPE(TypeName, PolicyValidator, DefaultType, ...)                                          \
static constexpr auto tuple##TypeName = hana::tuple_t<__VA_ARGS__>;                                       \
static constexpr auto predicate##TypeName = hana::compose(hana::trait<PolicyValidator>, hana::decltype_); \
                                                                                                          \
static constexpr auto found##TypeName = hana::find_if(tuple##TypeName, predicate##TypeName);              \
static constexpr auto default##TypeName = hana::just(hana::type_c<DefaultType>);                          \
                                                                                                          \
typedef typename std::remove_reference<decltype(                                                          \
        hana::if_(found##TypeName != hana::nothing, found##TypeName, default##TypeName).value()           \
    )>::type::type TypeName;                                                                              \
                                                                                                          \
static_assert(decltype(hana::count_if(tuple##TypeName, predicate##TypeName) <= hana::size_c<1>)::value,   \
    "Duplicate parameters in group '" #TypeName "'.");

The part that might need some explanations is the type definition near the end. found##TypeName and default##TypeName are both of the type hana::optional> for some T. We want to access the inner type T and add it as a type member to our policy class. hana::optional>::value() extracts the content of the optional and returns a hana::type &. We then get the type, remove the reference and access the inner type.

Leave a Reply

Your email address will not be published. Required fields are marked *