2

I have the following code where I implement dispatching on runtime value to interpret the data in certain way(in this toy example data can be either uint8_t or short).

Code seems to work, but I am wondering if I can somehow microoptimize the code so that when I have a hit(processing function matches) processing is stopped (currently even if first element of tuple is a "handler" entire tuple is iterated over at runtime).

#include <boost/mp11/tuple.hpp>
#include <iostream>

uint8_t data[4] = {0,1,100,2};

template<int runtimeId, typename T>
struct kindToType{
    static constexpr int id =  runtimeId;
    using type = T;
};

 const auto print  =[]<typename T> (const T* data){
    if constexpr(std::is_same_v<short, std::remove_cvref_t<T>>){
        const short* values = (const short*)data;
        std::cout << values[0] << "  " << values[1] << std::endl;
    } else if constexpr(std::is_same_v<uint8_t, std::remove_cvref_t<T>>){
        const uint8_t* values = (const uint8_t*)data;
    std::cout << (int)values[0] << "  " << (int)values[1]<< "  " << (int)values[2] << "  " << (int)values[3] << std::endl;;
    }
};

static constexpr std::tuple<kindToType<10, uint8_t>, kindToType<11, short>> mappings{};

void dispatch(int kind){
    boost::mp11::tuple_for_each(mappings, [kind]<typename Mapping>(const Mapping&) {
    if (Mapping::id == kind)
    {
        print((typename Mapping::type*)data);
    }
    });
}
int main()
{
    // no guarantee that kind is index like(e.g. for two values
    // it can have values 47 and 1701)
    dispatch(10);
    dispatch(11);
}

Notes:

  • I can not/want to use std::variant.
  • I do not want to use std::map or std::unordered map(where value is std::function)
  • I know this is premature optimization(even 10 integer comparisons is cheap assuming handlers do nontrivial amount of work).
  • my handlers are unique, i.e. it is std::map like thing, not std::multimap like thing so it is fine to break;.
  • kind of id used for runtime values is not guaranteed to have values in [0, n-1].
  • I am fine with C++20 solution as long as it is implemented in at least 1 compiler.
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277

1 Answers1

2

The runtime performance of this heavily depends on the size of your tuple. You can make your own for_each_tuple implementation that does an early out when your function gets executed:

template<typename FuncTuple, typename Selector>
void tuple_for_each(FuncTuple const& funcTuple, Selector selector) 
{
    std::apply([selector](auto const& ...funcs)
    {
        (void)(selector(funcs) || ...);
    }, funcTuple);
}

your dispatch would then look like this:

void dispatch(int kind)
{
    tuple_for_each(mappings, [kind]<typename Mapping>(const Mapping&) 
    {
        std::cout << "loop, ";
        if (Mapping::id == kind)
        {
            print((typename Mapping::type*)data);
            return true;
        }
        return false;
    });
}

If you get rid of the template in your lambda and use auto instead this code will compile with C++17. We use operator short circuiting to our advantage so the compiler will provide an early out for us. Here is the full code.

Also, note that the cast (const short*)data is UB.

Timo
  • 9,269
  • 2
  • 28
  • 58
  • regarding UB: because alignment requirements? – NoSenseEtAl Aug 12 '20 at 20:59
  • 1
    @NoSenseEtAl because `data` is a char array, not a short array. Your C style cast boils down to a `reinterpret_cast` which is only allowed to cast to a type `T` if the source is actually holding an object of type `T`. To be fair, the cast itself isn't UB but the dereferencing of the resulting pointer afterwards. Also, you most likely won't run into any issues because you're casting between pointers of integral types, but strictly speaking this is still UB nonetheless. – Timo Aug 12 '20 at 21:16
  • Note tho, that the other way around is actually fine. If you had a short array, you could cast it to a char pointer and dereference that pointer. This is because conversions to `(signed/unsigned) char` and `std::byte` are explicitly allowed, which can be used to read the [representation](https://en.cppreference.com/w/cpp/language/object#Object_representation_and_value_representation) of an object. – Timo Aug 12 '20 at 21:24
  • yes I knew about char* and friends, they are also exempt in strict aliasing rules. But I did not know that it is UB to read char array as short(beside alignment).... even -fsanitize=undefined does not complain :) – NoSenseEtAl Aug 12 '20 at 21:40
  • I've asked a [similar question](https://stackoverflow.com/q/59716273/6205379) a while back. The premise is slightly different but the rules should apply here as well. – Timo Aug 13 '20 at 06:48
  • getting highly offtopic, but I think it might change in C++23 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0593r4.html – NoSenseEtAl Aug 13 '20 at 09:02
  • 1
    Yeah I'm aware of the papaer and I hope it'll be accepted. – Timo Aug 13 '20 at 09:28
  • 1
    @NoSenseEtAl That was adopted for C++20 (well, [this version](https://wg21.link/p0593)). – Barry Aug 13 '20 at 19:05
  • @Barry great, IDK that, but I was just thingking about that since I saw it mentioned in a recent popular question... https://stackoverflow.com/questions/63379066/is-using-malloc-for-int-undefined-behavior-until-c20/63379211#63379211 P.S. I was expecting that your comment will be: it is one line in mp11 :) – NoSenseEtAl Aug 14 '20 at 08:08
  • 1
    @NoSenseEtAl Nope, Timo's got it right here - Mp11 doesn't have such a thing. And the solution here is pretty short :-) – Barry Aug 14 '20 at 12:54
  • @Barry shame, because although this solution is relatively short it becomes quite ugly/impossible if I want to fwd the return result of handler functions back to the caller of the tuple_for_each. – NoSenseEtAl Aug 14 '20 at 13:17
  • accepting, only code review comment I have is that tuple_for_each is no longer good fn name – NoSenseEtAl Aug 19 '20 at 09:33