Ordering multiple function calls during compile time

76 Views Asked by At

I have a function:

template<typename... Args>
auto CallItemsInOrder(std::tuple<Args...> items)
{
    // Magic here!
}

Where each item of the tuple has two qualities: A priority for which it should be called, and a function overload to call it with:

// DoWork calls are NOT constexpr
template<> void DoWork(int item)         { ... }
template<> void DoWork(std::string item) { ... }

// std::string is higher priority than int, gets called first
// Priorities don't have to be unique or sequential, to allow future customization
template<> struct WorkPriority<int>         { static constexpr int = 0; }
template<> struct WorkPriority<std::string> { static constexpr int = 10; }

Such that, when CallItemsInOrder is called with that tuple, it should be able to be optimized down to this at compiletime:

template<>
auto CallItemsInOrder(std::tuple<int, std::string> items)
{
    DoWork(std::get<1>(items));
    DoWork(std::get<0>(items));
}

This is something I could imagine is easy to do without a hundred lines of code using Boost.Hana, but frankly I'm having trouble understanding the docs. Any help would be appreciated!

2

There are 2 best solutions below

2
Jarod42 On BEST ANSWER

"Just" sort the indexes by priority (thanks to constexpr function):

template<typename... Args>
auto CallItemsInOrder(std::tuple<Args...> items)
{
    [&]<std::size_t ... Is>(std::index_sequence<Is...>){
        constexpr auto arr = [](){
            // priority, index pairs
            std::array<std::pair<int, std::size_t>, sizeof...(Is)> res{{
                {WorkPriority<std::tuple_element_t<Is, std::tuple<Args...>>>::value, Is}...
            }};
            std::sort(res.begin(), res.end(), std::greater{}); // std::partial_sort, custom comparer, ...
            return res;
        }();

        // now just call DoWork(std::get<arr[Is].index>(items)) in "loop".
        (DoWork(std::get<std::get<1>(arr[Is])>(items)), ...);
    }(std::index_sequence_for<Args...>());
}

Demo

2
Pepijn Kramer On

I made this example on how you can approach this with a client syntax looking like this : do_work_in_order_of_priority(1, "string 1"s, 1.0, "string 2"s);

It will output :

do_work(string "string 1")
do_work(string "string 2")
do_work(int 1)
do_work(double 1)

Based on a type priority specified by a std::variant<std::string,int,double>.

Online demo here : https://onlinegdb.com/moMu2kF5E

#include <cassert>
#include <iostream>
#include <type_traits>
#include <variant>
#include <string>
#include <iomanip>

using namespace std::string_literals;

// Using a variant type to specify the priority of types
// the actual variant is not used in implementation
using variant_t = std::variant<std::string, int, double>; // order in variant is order in which to call functions too

// declare functions first before templates.

void do_work(int value)
{
    std::cout << "do_work(int " << value << ")\n";
}

void do_work(double value)
{
    std::cout << "do_work(double " << value << ")\n";
}

void do_work(const std::string& value)
{
    std::cout << "do_work(string " << std::quoted(value) << ")\n";
}

// recursive part of looping over both the variant types
// and the argument types
namespace details
{
    template<typename variant_type_t, typename arg_t>
    constexpr std::size_t handle(arg_t arg)
    {
        if constexpr (std::is_same_v<variant_type_t, arg_t>)
        {
            do_work(arg); // <== this requires all overloads of function to be already known, before template is instantiated
            return 1ul;
        }
        else
        {
            return 0ul;
        }
    }

    template<std::size_t variant_index_v, typename... args_t>
    static constexpr std::size_t do_work_for_variant_type(args_t&&... args)
    {
        using variant_type_t = std::remove_reference_t<decltype(std::get<variant_index_v>(variant_t{}))>;
        std::size_t items_handled{0ul};

        ((items_handled += handle<variant_type_t>(args)),...);

        // recurse
        if constexpr ((variant_index_v+1ul) < std::variant_size_v<variant_t>)
        {
            items_handled += do_work_for_variant_type<variant_index_v + 1ul>(std::forward<args_t>(args)...);
        }

        return items_handled;
    }
}

template<typename... args_t>
static constexpr void do_work_in_order_of_priority(args_t&&... args)
{
    constexpr auto number_of_items_to_handle = sizeof...(args_t);
    // todo figure out how to make items_handled constexpr too
    auto items_handled = details::do_work_for_variant_type<0ul>(std::forward<args_t>(args)...);
    assert(number_of_items_to_handle == items_handled); 
}

int main()
{
    do_work_in_order_of_priority(1, "string 1"s, 1.0, "string 2"s);
    return 0;
}

For real production code I would like to have done the following too, but I don't have the time to do that:

  • No need to declare do_work before the template
  • change the assert into a static_assert