15

I'm just starting to use C++11's <random> header for the first time, but there are still some things that seem a bit mysterious. This question is about the intended, idiomatic, best-practice way to accomplish a very simple task.

Currently, in one part of my code I have something like this:

std::default_random_engine eng {std::random_device{}()};
std::uniform_int_distribution<> random_up_to_A {0, A};
std::uniform_int_distribution<> random_up_to_B {0, B};
std::uniform_int_distribution<> random_up_to_some_other_constant {0, some_other_constant};

and then when I want an integer between 0 and B I call random_up_to_B(eng).

Since this is starting to look a bit silly, I want to implement a function rnd such that rnd(n, eng) returns a random integer between 0 and n.

Something like the following ought to work

template <class URNG>
int rnd(int n, URNG &eng) {
    std::uniform_int_distribution<> dist {0, n};
    return dist(eng);
}

but that involves creating a new distribution object every time, and I get the impression that's not the way you're supposed to do it.

So my question is, what is the intended, best-practice way to accomplish this simple task, using the abstractions provided by the <random> header? I ask because I'm bound to want to do much more complicated things than this later on, and I want to make sure I'm using this system in the right way.

N. Virgo
  • 7,970
  • 11
  • 44
  • 65
  • Do you have any indication that creating a `uniform_int_distribution` is expensive? I suspect it isn't. You might find some guidance in the `boost` documentation which was the genesis of `random`. – Mark Ransom Jul 04 '14 at 04:24
  • @MarkRansom I would imagine that creating a new `uniform_int_distribution` can be inlined and has no cost at all. But for other distributions this might not be the case, since the implementation might need to store state. The question is not so much about implementing this specific thing, as understanding how `` is intended to be used in general. – N. Virgo Jul 04 '14 at 04:26
  • 1
    Distributions are intended to be cheap to construct. It's the random engine that's expensive. – T.C. Jul 04 '14 at 05:44
  • @T.C. that depends on the distribution. For some algorithms for sampling from some distributions, you can save CPU cycles by generating several values simultaneously. I imagine some of the distributions in `` use such a technique, otherwise there wouldn't have been any point in making them objects that can be instantiated, as opposed to just functions. If a distribution does work like that then creating the object every time would be less efficient. (Even if this conjecture is wrong, there must be *some* reason why the API is designed this way.) – N. Virgo Jul 04 '14 at 06:13
  • @Nathaniel Yes, there are cases where it's not as efficient (normal distribution using the Box-Mueller transform, for example). Even then it's usually far cheaper than the engine. (The state sequence of `std::mt19937` is, well, 19937 bits.) – T.C. Jul 04 '14 at 06:26

3 Answers3

7

uniform_int_distribution should not be expensive to construct, so creating one every time with new limits should be OK. However, there is a way to use the same object with new limits, but it is cumbersome.

uniform_int_distribution::operator() has an overload that takes a uniform_int_distribution::param_type object which can specify the new limits to be used, but param_type itself is an opaque type, and there's no portable way to construct one except extracting it from an existing uniform_int_distribution instance. For instance, the following function can be used to construct a uniform_int_distribution::param_type.

std::uniform_int_distribution<>::param_type
    make_param_type(int min, int max)
{
    return std::uniform_int_distribution<>(min, max).param();
}

Pass these to operator() and the generated result will be in the specified range.

Live demo

So if you really want to reuse the same uniform_int_distribution, create and save multiple instance of param_type using the function above, and use these when calling operator().


The answer above is inaccurate, because the standard does specify that the param_type can be constructed from the same distribution arguments as those used by the corresponding distribution type's constructor. Thanks to @T.C. for pointing this out.

From §26.5.1.6/9 [rand.req.dist]

For each of the constructors of D taking arguments corresponding to parameters of the distribution, P shall have a corresponding constructor subject to the same requirements and taking arguments identical in number, type, and default values. ...

So we don't need to construct the distribution object needlessly only to extract the param_type. Instead the make_param_type function can be modified to

template <typename Distribution, typename... Args>
typename Distribution::param_type make_param_type(Args&&... args)
{
  return typename Distribution::param_type(std::forward<Args>(args)...);
}

which can be used as

make_param_type<std::uniform_int_distribution<>>(0, 10)

Live demo

Community
  • 1
  • 1
Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • It seems you can set the `param_type` object's parameters without getting them from an existing distribution. Could you look at my own answer and see if there's anything wrong with it? (In particular you mentioned portability, and I don't know whether I'm doing something that would mess with that.) – N. Virgo Jul 04 '14 at 06:32
7

Answering my own question: by adapting an example found in this document, the following appears to be the correct way to implement a function returning a random integer between 0 and n-1 inclusive:

template<class URNG>
int rnd(int n, URNG &engine) {
    using dist_t = std::uniform_int_distribution<>;
    using param_t = dist_t::param_type;

    static dist_t dist;
    param_t params{0,n-1};

    return dist(engine, params);
}

To make it thread-safe one must avoid the static declaration. One possibility is to make a convenience class along these lines, which is what I'm using in my own code:

template<class URNG>
class Random {
public:        
    Random(): engine(std::random_device{}()) {}
    Random(typename std::result_of<URNG()>::type seed): engine(seed) {}

    int integer(int n) {
        std::uniform_int_distribution<>::param_type params {0, n-1};
        return int_dist(engine, params);
    }

private:
    URNG engine;
    std::uniform_int_distribution<> int_dist;
};

This is instantiated with (for example) Random<std::default_random_engine> rnd, and the random integers can then be obtained with rnd.integer(n). Methods for sampling from other distributions can easily be added to this class.

To repeat what I said in the comments, reusing the distribution object is probably unnecessary for the specific task of uniformly sampling integers, but for other distributions I think this will be more efficient than creating it every time, because there are some algorithms for sampling from some distributions that can save CPU cycles by generating multiple values simultaneously. (In principle even uniform_int_distribution could do this, via SIMD vectorisation.) If you can't increase efficiency by retaining the distribution object then it's hard to imagine why they would have designed the API this way.

Hooray for C++ and its needless complexity! This concludes an afternoon's work accomplishing a simple five-minute task, but at least I have a much better idea what I'm doing now.

N. Virgo
  • 7,970
  • 11
  • 44
  • 65
  • 2
    I thought about posting this also, but decided against it because it isn't portable. There's no requirement for `param_type` to have an accessible 2 argument constructor, although what you've done works on gcc, clang and VS2013. – Praetorian Jul 04 '14 at 07:20
  • Your ideal solution is to write this code for whatever implementation you know it works on, and for other implementations, do what @Praetorian mentioned. – user541686 Jul 04 '14 at 07:21
  • Its interesting that the proposal you've linked to does state that this is how you construct the `param_type`, and that it is a nested type. However, the standard doesn't state anything about its construction, and explicitly states that it's unspecified whether it is a nested type or a typedef. Wonder what caused the changes between the proposal and inclusion in the standard. – Praetorian Jul 04 '14 at 07:34
  • 2
    @Praetorian Actually there is a requirement. N3797 §26.5.1.6/p9: "For each of the constructors of D taking arguments corresponding to parameters of the distribution, P shall have a corresponding constructor subject to the same requirements and taking arguments identical in number, type, and default values. Moreover, for each of the member functions of D that return values corresponding to parameters of the distribution, P shall have a corresponding member function with the identical name, type, and semantics." – T.C. Jul 04 '14 at 14:02
  • @T.C. Thank you for that, now it makes sense why it's specified the way it is, and not in a more obvious manner. Nathaniel: I've updated my answer with a new version of the function to create `param_type` objects. – Praetorian Jul 04 '14 at 17:27
  • 1
    `static` makes your code non-threadsafe and adds nonlocal state, which makes your program more difficult to reason about and may hinder performance. If you really want to reuse the distribution object, keep it along with your engine in algorithm state. – ecatmur Jul 04 '14 at 17:46
  • @ecatmur I agree with you. It's quite annoying that all the available example code uses `static`, so I've added an example class that keeps the distribution object as a class member. – N. Virgo Jul 05 '14 at 03:46
  • Hmm, I'm fairly sure that you'd want `engine` declared as of type `URNG` rather than `std::default_random_engine`. – T.C. Jul 05 '14 at 18:39
2

The idiomatic way to generate code according to varying parameters is to create distribution objects as needed, per Vary range of uniform_int_distribution:

std::random_device rd;
std::default_random_engine eng{rd()};
int n = std::uniform_int_distribution<>{0, A}(eng);

If you are concerned that performance may be hindered by failing to fully exploit the distribution's internal state, you can use a single distribution and pass it different parameters each time:

std::random_device rd;
std::default_random_engine eng{rd()};
std::uniform_int_distribution<> dist;
int n = dist(eng, decltype(dist)::param_type{0, A});

If this seems complicated, consider that for most purposes you will generate random numbers according to the same distribution with the same parameters (hence the distribution constructor taking parameters); by varying parameters you are already entering into advanced territory.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366