1

I am looking to only print out the "grouped" enums but having trouble getting the expected behaviour. So basically printing out all the enums from the specified base Values until there isn't any subsequent consecutive enum value. Each "group" of enum can be determined by ANDing with a mask 0xFFFF0000

The trick is I could iterate over _map enum but then there won't be an easy way to check whether the corresponding key exists. find method takes a key so that won't help.

P.S: _map already exists for 'other' purposes so I can't change that

enum class Values : uint32_t
{
  one    =  0x00000000,
  oneOne =  0x00000001,
  oneTwo =  0x00000002,

  two =     0x00010000,
  twoOne =  0x00010001,
  twoTwo =  0x00010002,

  three    =  0x00020000,
  threeOne =  0x00020001,
  threeTwo =  0x00020002,
  //...


  MAX
};

std::unordered_map<std::string, Values> _map =
{
    {"one",     Values::one},
    {"oneOne",  Values::oneOne},
    {"oneTwo",  Values::oneTwo},
    {"two",     Values::two},
    {"twoOne",  Values::twoOne},
    {"twoTwo",  Values::twoTwo}
};

What I came up with is the following but there isn't a way to "break" where the enum value doesn't exist.

void foo(Values base)
{
    uint32_t mask = static_cast<uint32_t>(base) & 0xffff0000;

    for (Values i = base; i < Values::MAX; i = static_cast<Values>(static_cast<uint32_t>(i) + 1)) 
    {
        uint32_t curMask = static_cast<uint32_t>(i) & 0xffff0000;

        if (curMask != mask) 
        {
            break;  // stop if we've reached a different upper 16 bits value
        }
        
        std::cout << std::hex << static_cast<uint32_t>(i) << "\n";
    }
}

// expected calls with expected output
foo(Values::one);      // should print: one, oneOne, oneTwo
foo(Values::oneOne);   // should print: oneOne, oneTwo
foo(Values::twoTwo);   // should print: twoTwo
xyf
  • 664
  • 1
  • 6
  • 16
  • Maybe split into two enums? One for groups (higher 16 bits) and groups for lower bit values. But in the end, enums are just not enumerable in C++. So you probably need two way mapping anyway. And maybe if things are not efficient enough... try thinking of different approaches. – Pepijn Kramer Feb 25 '23 at 06:03
  • *"`_map` already exists for 'other' purposes so I can't change that"* -- can't change it at all, or merely cannot change it in a way that would break existing code? (There is a difference if the existing code does not make too many assumptions about the type of `_map`.) If it's "not change at all", what is stopping you from defining a second map (right next to where `_map` is defined) going in the reverse direction? – JaMiT Feb 25 '23 at 06:03
  • @JaMiT it's just `_map` is already used elsewhere in the application in simple words. Is defining a second map really that effective? Say enum grows to over 1000 values, meaning you'd have two maps each of which mapping 1000 values...is it sustainable? probably not. – xyf Feb 25 '23 at 07:03
  • @xyf "already used" suggests "cannot change in a way that would break existing code". That could allow a replacement container with similar-enough syntax (e.g. [Boost.Bimap](https://www.boost.org/doc/libs/1_81_0/libs/bimap/doc/html/index.html)), but the question as it is currently written explicitly forbids such an answer. – JaMiT Feb 25 '23 at 07:15
  • Identifiers beginning with an underscore are reserved in the global namespace. Defining `_map` at file scope is undefined behaviour. [more info](https://stackoverflow.com/a/228797/1563833) – Wyck Mar 02 '23 at 02:54

2 Answers2

1

If you have

const std::unordered_map<std::string, Values> _map = {
    {"one", Values::one},       
    {"oneOne", Values::oneOne},
    {"oneTwo", Values::oneTwo}, 
    {"two", Values::two},
    {"twoOne", Values::twoOne}, 
    {"twoTwo", Values::twoTwo}};

I suggest adding a reverse lookup std::map

const auto _rmap = []{
    std::map<Values, decltype(_map.cbegin())> rv;
    for(auto it = _map.cbegin(); it != _map.cend(); ++it)
        rv.emplace(it->second, it);
    return rv;
}();

... to be able to use std::map::upper_bound to get an iterator to the first element after the subgroup you are interested in:

void foo(Values base) {
    if (auto it = _rmap.find(base); it != _rmap.end()) {
        // mask contains the highest possible value in the subgroup:
        auto mask = static_cast<Values>(static_cast<uint32_t>(base) | 0xffff);

        // end is an iterator to the first element after `mask`
        auto end = _rmap.upper_bound(mask);

        for (; it != end; ++it) {
            // dereference the iterator into the original map:
            const auto& [str, val] = *it->second;

            std::cout << str << '\t' << static_cast<uint32_t>(val) << '\n';
        }
    }
}

Demo



If you prefer to store std::strings in the reverse lookup std::map instead of iterators, that works fine too:

#include <algorithm> // std::transform

const auto _rmap = [] {
    std::map<Values, std::string> rv;
    std::transform(_map.begin(), _map.end(), std::inserter(rv, rv.end()),
                   [](auto&& p) { return std::pair{p.second, p.first}; });
    return rv;
}();

void foo(Values base) {
    if (auto it = _rmap.find(base); it != _rmap.end()) {
        auto mask = static_cast<Values>(static_cast<uint32_t>(base) | 0xffff);
        auto end = _rmap.upper_bound(mask);
        for (; it != end; ++it) {
            const auto& [val, str] = *it;

            std::cout << str << '\t' << static_cast<uint32_t>(val) << '\n';
        }
    }
}

Demo

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • interesting perspective. 1) you used `std::map` so you could use a `upper_bound` to get the *next* value as `end`, yes? 2) any reason behind using `decltype` to get the type of the `_map`'s key instead of using `std::string`? – xyf Feb 25 '23 at 07:32
  • @xyf 1) Exactly. `mask` contains the highest valid value for the subgroup you are querying and `upper_bound` will fetch the next value after that. 2) I use `decltype` to get the _iterator_ type in the original map. That is: `std::unordered_map::const_iterator` which is a lot to type and if you change the map type in the future, the `decltype(_map.cbegin())` declaration will still work. – Ted Lyngmo Feb 25 '23 at 07:38
  • One thing that's still confusing is the reason behind not having `Values, std::string` as the type of `_rmap` and instead having `Values, std::unordered_map::const_iterator` – xyf Feb 26 '23 at 05:36
  • @xyf Both versions works fine so you can select either one in the `std::map`. With an iterator you get a pointer into the original map so you don't have to store copies of all the strings. If you store `string`s instead of iterators it will on the other hand be slightly faster since you don't have to do one extra indirection. – Ted Lyngmo Feb 26 '23 at 08:23
  • Yes, the copies won't needed to be stored if iterator is used a value however is its cost higher than if a string copy was instead created? Probably yes, right? (as If I interpret correctly, you affirm to it) – xyf Feb 26 '23 at 19:27
  • @xyf There is a small time cost of creating string copies vs. copying iterators and there's also a storage cost. If that's not a problem and you do not need to have pointers(iterators) pointing into the original `unordered_map` (which can be very handy at times), you can go for copying the strings. In a scenario where `unordered_map` is non-`const` and its nodes can be extracted, _Keys_ changed and reinstered, you definitely want to store iterators instead since you'll then get the updated values for free. Otherwise you'd have to update both maps. – Ted Lyngmo Feb 26 '23 at 19:36
  • @xyf Yes, changing the original map _Keys_ reflects through the iterators. [Example](https://godbolt.org/z/EsxheWEGK). – Ted Lyngmo Feb 26 '23 at 23:41
  • Btw, did any of the answers answer your question? If you'd like you can accept an answer. – Ted Lyngmo Feb 26 '23 at 23:42
  • Yes, it did. Thanks. the map is supposed to be static and not modified during runtime so just using `std::string` instead of an iterator to map makes more sense then. One last question: does `_rmap` do any different from merely looping through `_map` and storing `second` and `first` into `rv`? I am just having a bit of a hard time breaking this syntax up... – xyf Feb 27 '23 at 03:20
  • @xyf Great! To find a _Value_ in `_map` you would have to search through ~half the map and since `_map` is an `unordered_map`, looping through the entries will come out of order so you would need to loop through the whole map to find all entries in the subgroup you are interested in, store and sort them. If you only have very few entries in `_map` neither of these things will probably matter - but if that's the case, then using an `unordered_map` could also be questioned. Using the `_map`/`_rmap` combination will scale well and still be pretty quick even with many throusands of elements. – Ted Lyngmo Feb 27 '23 at 08:39
0

Just reverse _map into another map and use that to help identify which "Values" are valid.

I modified your code slightly and declared reverseMap in your foo function. But you could move the initialization of reverseMap to be a global or member variable somewhere else.

struct ReverseMapContainer
{
    std::unordered_map<uint32_t, std::string> reverseMap;
    ReverseMapContainer()
    {
        for (const auto& v : _map)
        {
            reverseMap.insert({ (uint32_t)(v.second), v.first });
        }
    }
};

void foo(Values base)
{
    static ReverseMapContainer rmc;
    const auto& reverseMap = rmc.reverseMap;

    uint32_t mask = static_cast<uint32_t>(base) & 0xffff0000;
    uint32_t stop = mask + 0x00010000;

    for (uint32_t i = (uint32_t)base; i < stop; i++)
    {
        auto itor = reverseMap.find(i);
        if (itor != reverseMap.end())
        {
            std::cout << itor->second << " ";
        }
        else
        {
            break;
        }
    }
    std::cout << "\n";
}

And then to test it with the same criteria you had:

int main()
{
    // expected calls with expected output
    foo(Values::one);      // should print: one, oneOne, oneTwo
    foo(Values::oneOne);   // should print: oneOne, oneTwo
    foo(Values::twoTwo);   // should print: twoTwo
}

Result:

enter image description here

selbie
  • 100,020
  • 15
  • 103
  • 173
  • Is there a more efficient way perhaps in a single pass than storing a reverse map? – xyf Feb 25 '23 at 05:48
  • @xyf - I reject your hypothesis. :) There's nothing inefficient with the above (especially after the edit I just made). As I suggested, move the initialization of the reverseMap outside of the function so it's only done once. Then it will be really hard to make the case that it needs better performance. – selbie Feb 25 '23 at 06:00
  • inefficient is subjective; it depends on who we compare against. I was mainly wondering if there was a single pass solution which probably would be more efficient than not a single pass, yes? – xyf Feb 25 '23 at 06:11
  • 1
    **efficiency is not subjective**. It's something you measure and benchmark against stated performance goals. There's no free lunch with doing reverse lookups. But there is free code - such as the edit I just made. – selbie Feb 25 '23 at 06:24
  • Yes, and I was comparing how a single pass most likely would end up being more efficient than the not a single-pass, but fair. Also what exactly is your edit that's more performant than the prior code? Is there really a need to have a separate `ReverseMapContainer` struct rather than making `ReverseMapContainer` a free function? – xyf Feb 25 '23 at 06:57
  • @xyf - I just declared ReverseMapContainer as a class with a constructor such that it would be initialized exactly once in a thread safe way as a static variable inside the foo function. You can declare and initialize the reverse map any way you want (global variable, member of a class, inline inside the function, etc....) that makes sense for your code. – selbie Feb 25 '23 at 07:21