5

I use the C++ random number utility library in quite a few places. It might not be perfectly comfortable (e.g. no base class for an arbitrary distribution), but - I've learned to live with it.

Now I happen to need to uniformly sample values from an enumerated type. I know, there's a question on that on SO already:

generating random enums

however, that one:

  1. Assumes all enum values are contiguous, i.e. it won't work for

    enum Color { Red = 1, Green = 2, Blue = 4 }
    

    where we want each of these three values to be sampled with probability 1/3.

  2. Does not provide the functionality of std::uniform_distribution<>, i.e. it doesn't work with a random engine you pass it and so on.

Obviously I can't use std::uniform_int_distribution<Color>, if only for reason 1 above. What should I do instead?

Notes:

  • The code must be generic, i.e. the enum type would be a template parameter.
  • Since it is likely I would need some instrumentation over just the rough enum, you may assume I have it; just state your assumption explicitly.
  • Specifically, and if it helps, suppose I use Better Enums, making me fully decked out with all the bells and whistles.
  • If there's somehow an idiomatic way of doing this not involving any such instrumentation, that would make for a great answer, but I doubt it.
  • C++11/14-only solutions are acceptable.
  • Multiple enum identifiers with the same value do not get double the frequency, they're just aliases of each other. If you have a simple solution assuming these do not exist, that would also be relevant, though suboptimal.
Community
  • 1
  • 1
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • "Idiomatic" and using a third party reflective library are mutually exclusive. What about the library is giving you trouble? It even gives you an example of iterating over it with a ranged-based for. – uh oh somebody needs a pupper Aug 15 '16 at 13:58
  • @uhohsomebodyneedsapupper: You're semantically right. The library is not giving me trouble at all. – einpoklum Aug 15 '16 at 13:59
  • You could remove those values, let the compiler put default values (starting from 0), and initialize an auxiliary `map` with those values, which you could then use along with `std::uniform_distribution<>`. – barak manos Aug 15 '16 at 14:01
  • If two declared enums have the same value, do you want the distribution to weight their distribution accordingly? e.g. `enum Color { White, Black, Gray, Grey = Gray }`? – ecatmur Aug 15 '16 at 14:01
  • So..what is the issue then? You already know that C++ doesn't have reflection, that's why you're using a reflection library. And there's tons of questions on iterating over enums on SO using standard C++. [Selecting a valid random enum value in a general way](https://stackoverflow.com/questions/25357545/selecting-a-valid-random-enum-value-in-a-general-way?rq=1) might help. – uh oh somebody needs a pupper Aug 15 '16 at 14:02
  • @ecatmur: Valid point, see edit. – einpoklum Aug 15 '16 at 14:03
  • @uhohsomebodyneedsapupper: "*"Idiomatic" and using a third party reflective library are mutually exclusive.*" Nonsense. "idiomatic" does not mean "thing that comes from the C++ standard." Boost invented tons of idioms all on its own. And they still do. – Nicol Bolas Aug 15 '16 at 14:04
  • @NicolBolas I didn't realize reflection was idiomatic in C++. My apologies. – uh oh somebody needs a pupper Aug 15 '16 at 14:06
  • @NicolBolas: Well, using a third-party not-very-popular enum-reflection library is itself not idiomatic in C++, so nothing based on it can claim to be idiomatic. I could say I'm looking for a "conditional idiomatic solution"... – einpoklum Aug 15 '16 at 14:06
  • @einpoklum: I think there is no "idiomatic" solution, generating a distribution is just a subtle and complex thing and you should do it out explicitly, in exactly the way you want, for maximum clarity. I don't know any programming language that attempts to define an "idiom" for the problem you are raising. – Chris Beck Aug 15 '16 at 14:13
  • @ChrisBeck: Generating a uniform distribution over a set of integral values is neither subtle nor complex, mathematically; I don't see why it should be easily possible to have programmatically. – einpoklum Aug 15 '16 at 14:14
  • 1
    @einpoklum: Maybe what you really want is http://en.cppreference.com/w/cpp/numeric/random/discrete_distribution ? Sorry but I think any code that tries to deduce the distribution parameters from the enum is going to be, at best, an application-specific utility, and at worst, shite. – Chris Beck Aug 15 '16 at 14:17
  • @ChrisBeck: That's an interesting idea. I had not used `std::discrete_distribution`, need to give it some thought. – einpoklum Aug 15 '16 at 14:21

4 Answers4

4

With use of Better Enums, this problem may be resolved this way:

template<typename T>
typename T get_uniform_value(std::default_random_engine& eng)
{
    std::uniform_int_distribution<int> dist(0, T::_size() - 1);
    return T::_values()[dist(eng)];
}

Usage example:

BETTER_ENUM(Channel, int, Red, Green = 2, Blue) // Enum to generate random values of
...
std::default_random_engine rng(std::random_device{}());
Channel r = get_uniform_value<Channel>(rng); // Uniformly distributed between 0, 2 and 3
alexeykuzmin0
  • 6,344
  • 2
  • 28
  • 51
2

Here's three implementations of a distribution, in order of ascending complexity:

First, if we can rely on values being distinct or are OK with repeat values being overweighted we can just index the _values() container:

template<class Enum>
struct SimpleEnumDistribution
{
    std::uniform_int_distribution<typename Enum::_integral> dist{0, Enum::_size() - 1};
    template<class Generator> Enum operator()(Generator& g) { return Enum::_values()[dist(g)]; }
};

Otherwise, we can use rejection sampling, precalculating the min and max of the range of enum values:

template<class Enum>
struct UniformEnumDistribution
{
    std::uniform_int_distribution<typename Enum::_integral> dist{
        *std::min_element(Enum::_values().begin(), Enum::_values().end()),
        *std::max_element(Enum::_values().begin(), Enum::_values().end())};
    template<class Generator> Enum operator()(Generator& g)
    {
        for (;;)
            if (auto value = Enum::_from_integral_nothrow(dist(g)))
                return *value;
    }
};

If this would be inefficient (perhaps the enum values are sparse) we can compute a lookup table on initialization:

template<class Enum>
struct FastUniformEnumDistribution
{
    std::uniform_int_distribution<std::size_t> dist;
    std::array<typename Enum::_integral, Enum::_size()> values;
    FastUniformEnumDistribution()
    {
        std::copy(Enum::_values().begin(), Enum::_values().end(), values.data());
        std::sort(values.begin(), values.end());
        dist.param(std::uniform_int_distribution<std::size_t>::param_type{0u, static_cast<std::size_t>(
            std::distance(values.begin(), std::unique(values.begin(), values.end())) - 1)});
    }
    template<class Generator> Enum operator()(Generator& g)
    {
        return Enum::_from_integral_unchecked(values[dist(g)]);
    }
};

Example.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • I guess that's good enough for me. Although this should be beefed up to meet all of the RandomNumberDistribution concept requirements. – einpoklum Aug 17 '16 at 12:56
1

I would say the more idiomatic would be to create an array and choosing index from the array:

 template <typename Rnd>
 Color RandomColor(Rnd& rnd)
 {
     const std::array<Color, 3u> colors {Color::Red, Color::Green, Color::Blue};

     std::uniform_int_distribution<int> dist(0, colors.size() - 1);
     return colors[dist(rnd)];
 }

Better Enums seems to allow to not create the array manually with Color::_values:

 template <typename BetterEnum, typename Rnd>
 BetterEnum RandomBetterEnum(Rnd& rnd)
 {
     std::uniform_int_distribution<int> dist(0, BetterEnum::_size() - 1);
     return BetterEnum::_values()[dist(rnd)];
 }
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • If you can't generalize your implementation to take the Enum type as a template parameter, then that's not good enough. – einpoklum Aug 15 '16 at 14:48
  • Without extra library, as we don't have reflection, you have to construct the array. It seems that the library you use provide this functionality, so it can be generalized for enum managed by BetterEnum. – Jarod42 Aug 15 '16 at 14:54
  • @Jarod42, sure you can. SFINAE on the return type using `typename std::enable_if::value, BetterEnum>::type` To be "more correct", you may want to use `size_t` instead of `int` but that's being very nit-picky. – Andrew Aug 15 '16 at 18:40
0

In the question you linked to, it is assumed that you want the uniform distribution over the enumerator values.

However, "uniform distribution over an enum-type" might also mean, uniform distribution over the range of the enum, which typically means, all the possible values of the underlying type which was selected by the implementation.

There are other fundamental problems:

In the case that you showed

enum Color { Red = 1, Green = 2, Blue = 4 }

Presumably, the uniform distribution that you want is from 0 to 7 (each enumerator possible OR'd together using bitmasks).

Suppose the enum were:

enum Color { Red = 1, Green = 2, Blue = 3 }

Then presumably you only want 1, 2, 3 in your distribution.

I think you can't expect the compiler or any template code to understand your intent -- any "enum -> uniform distribution" code would require hints so that it knows which enumerators should be |'d with combinations of the others and which ones are simply options.

So in short, I think you should do exactly what the question you linked to does, and generate the appropriate distribution over int's or whatever, and then static_cast it to the enum. And don't try to use some template solution that tries to read your mind for every possible enum.

Community
  • 1
  • 1
Chris Beck
  • 15,614
  • 4
  • 51
  • 87
  • In the Color example, the unfirom distribution should be 1 w.p. 1/3, 2 w.p. 1/3, 3 w.p. 1/3. I'll make that clearer. And - I can certainly expect this to be understood if I don't just define my enum that way, but use a more complex mechanism - see the notes. – einpoklum Aug 15 '16 at 14:09