h-2 / blog-comments

comment repository for my blog
1 stars 0 forks source link

Configuring algorithms in Modern C++ #8

Open h-2 opened 1 year ago

h-2 commented 1 year ago

When designing library code, one often wonders: "Are these all the parameters this function will ever need?" and "How can a user conveniently change one parameter without specifying the rest?" This post introduces some Modern C++ techniques you can use to make passing configuration options easy for your users while allowing you to add more options later on.

https://hannes.hauswedell.net/post/2023/03/30/algo_config/

youngjoel commented 8 months ago

In

/* And also the algorithm */
template <typename ...Ts>
auto algo(auto data, algo_config<Ts...> const & cfg)
{
    /* implementation */
}

Could you give some examples in the /* implementation */ block on how to instantiate variables of the passed type? Similar expansion in the constants /* implementation */ also.

youngjoel commented 8 months ago

For example, is this the most elegant way to use the passed type?

/* And also the algorithm */
template <typename ...Ts>
auto algo(auto data, algo_config<Ts...> const & cfg)
{
    /* implementation */
    typename decltype(cfg.int_type)::type x = 5;
    std::cout << x << std::endl;
}
youngjoel commented 8 months ago

Also, in this line:

This would allow us to omit align_config also when passing “type parameters” or constants.

Should that have been algo_config instead of align_config ?

h-2 commented 8 months ago

For example, is this the most elegant way to use the passed type?

Yeah, that would be a correct way of doing it. If you want to get the implementation part a bit shorter, at the cost of adding a custom type_identity and some more template foo, you could do the following:

/* We define a "type tag" so we can pass types as values */
template <typename T>
struct type_identity
{
    using type = T;

    static constexpr type_identity me{};

    template <typename T2>
    friend consteval bool operator==(type_identity, type_identity<T2>) noexcept
    {
        return std::is_same_v<T, T2>;
    }
};
template <typename T>
inline constinit type_identity<T> ttag{};

/* The config now becomes a template */
template <typename Tint_type = decltype(ttag<uint64_t>)>
struct algo_config
{
    bool heuristic42    = true;
    Tint_type int_type  = ttag<uint64_t>;
    size_t threads      = 4ull;
};

/* helper */
template <auto i>
using _t = typename decltype(i)::type;

/* And also the algorithm */
template <typename ...Ts>
auto algo(auto data, algo_config<Ts...> const & cfg)
{
    /* implementation */
    _t<cfg.int_type.me> x = 5;

    if constexpr (cfg.int_type.me == ttag<uint32_t>) // 32bit path
    {
        // ...
    }
    else // default path
    {
        // ...
    }
}

Explanation: Although type_identity contains no state, we cannot use cfg.int_type in constant expressions, so we can neither pass it as a template non-type argument, nor can we compare within if constexpr. However, if we add a static constexpr member object (me) to the type, we can use this to avoid lots of decltype and is_same_v cruft in the algo. You will have to decide whether the other changes are worth it though.

For the constants, it works much nicer right away:

template <typename ...Ts>
auto algo(auto data, algo_config<Ts...> const & cfg)
{
    /* implementation */
    if constexpr (cfg.use_simd.value) // SIMD implementation
    {
        // ... 
    }
    else // fallback implementation
    {
        // ...
    }
}

Should that have been algo_config instead of align_config ?

Thanks for pointing this out, will fix it in the next update of the blog!