Concepts / Vectors and Indexing in C++

Vectors and Indexing in C++

The vector works as a histogram: each index stands for a possible value, and each stored number records how often that value appeared.

  • Programming

Predicting What a Ranged Generator Returns

Suppose you want a program that simulates rolling a die. A die has faces numbered 1 through 6, so the program needs a random value that is always in that range, never a 0 and never a 7. This is the central problem this article addresses. A random number generator on its own does not know what range you care about. You need a tool that remembers the range you asked for and returns values only from inside it.

The source frames the goal in terms of a number x that is guaranteed to lie in a given range, such as the closed range from 1 to 10 written $[1,10]$, and that you can obtain through a simple expression rather than through low-level details every time. The usage pattern it aims for looks like this: you create a generator for a specific range, and then calling that generator produces one value from the range.

cpp
Notes on the code
  • Rand_int rnd(1, 10) creates a generator for the range from 1 to 10
  • rnd() produces one integer drawn from that range
  • The result is stored in the variable x

What do you think happens?

Before reading the class, predict: which values could x take after this code runs? Is it possible that x is 0 or 11?

Reveal answer
Answer:

x can be any integer from 1 through 10, including both endpoints. The values 0 and 11 are impossible, because the generator is built for the closed range from 1 to 10.

The range you pass to the constructor is the range that comes out. Keep this prediction in mind, because the rest of the article shows how the class enforces it.

Why Output Must Stay Inside a Range

A random number generator is most useful when you can control what it produces. If a simulation of a die accidentally produced a 9, the simulation would be describing a object that does not exist. Constraining output to a range is what makes random values usable for the purpose you have in mind. The range is part of the contract between the generator and the code that uses its values.

A range-constrained generator is worth contrasting with an unbounded one. An unbounded output can land anywhere the generator is capable of producing, so a program that uses it to fill histogram bins could try to store a count at a position that has no bin at all. The range-constrained version guarantees that every returned value has a place to go, which is why the histogram program later depends on the range being respected.

Inside Rand_int: Engine and Distribution

The source defines Rand_int as a class with a constructor that takes a low bound and a high bound, a function-call operator that returns one random integer per call, and two private members. The class is the tool that turns the one-line usage pattern into a working generator. The source notes that the class definition is expert-level code, but that using it is manageable in the first week of a C++ course, which is the perspective taken here.

The class is split into two parts that each do one job. The default_random_engine, named re, is the source of raw randomness. The uniform_int_distribution, named dist, takes that randomness and shapes it so that every integer in the range is equally likely. A uniform distribution is one in which all outcomes are equally likely, and this is the property the source has in mind when it describes the output as uniform.

callercalls rnd()operator()returns one integerdefault_random_engineresource of raw randomnessuniform_int_distributiondistmakes each value in rangeequally likelycall rnd()passes engineraw randomnessinteger in rangereturned value
When the caller invokes rnd(), the call enters operator(), which hands the engine to the distribution. The distribution returns an integer, and operator() passes that integer back to the caller.

Read the diagram as a trace of one call. The caller invokes operator(). Operator() hands the engine to the distribution. The engine supplies randomness, the distribution turns that randomness into an integer inside the stored range, and operator() returns that integer to the caller. Nothing in this path lets the caller reach into the engine or the distribution directly.

Counting Draws with a Histogram

A single draw tells you very little about whether a generator is fair. To check the distribution, the source counts many draws. The program creates a generator for the range from 0 through a constant max, makes a vector of size max plus one to hold the counts, and then draws 200 values, adding one to the matching slot each time. The vector works as a histogram: each index stands for a possible value, and each stored number records how often that value appeared.

cpp
Notes on the code
  • histogram has max + 1 slots, so indices 0 through max all have a place
  • rnd() returns a value from 0 through max, which is used directly as the index
  • The loop makes 200 draws, and each draw adds one to exactly one slot

The counting line ++histogram[rnd()] is the core of the program. The call rnd() produces an index between 0 and max, and the increment records one more occurrence at that index. Because the vector has max plus one slots, every possible index has a place to be counted. This matches the fact that the source uses a histogram vector with max plus one slots to count each value from 0 through max.

Worked example

Tracing four draws into the histogram

Suppose the generator returns the values 3, 6, 3, and 0, in that order, and the histogram has slots 0 through 6. What does the histogram contain afterward?

  1. 1
    Draw 1 returns 3

    The line ++histogram[3] increments slot 3, which now holds 1.

  2. 2
    Draw 2 returns 6

    The line ++histogram[6] increments slot 6, which now holds 1.

  3. 3
    Draw 3 returns 3

    The line ++histogram[3] increments slot 3 again, which now holds 2.

  4. 4
    Draw 4 returns 0

    The line ++histogram[0] increments slot 0, which now holds 1.

Result

Slot 0 holds 1, slot 3 holds 2, slot 6 holds 1, and every other slot holds 0.

index 0count 1index 1count 0index 2count 0index 3count 2index 4count 0index 5count 0index 6count 1
Each slot is an index from 0 through 6. The stored count shows how many draws returned that index. The draws were 3, 6, 3, and 0.

After the counting loop, a second loop writes a bar graph. For each index i, it prints the index, a tab, and then one star symbol for each counted occurrence. The source describes a healthy result as a reassuringly boring uniform distribution: the bars come out roughly the same length, with reasonable statistical variation from one value to the next. The bars are not expected to be exactly equal, because 200 draws is a finite sample. A uniform distribution shows roughly similar counts with statistical variation, not exactly identical counts.

Where an Off-by-One Error Hides

When a histogram program misbehaves, the useful question is where the result first stops matching your expectation. The source identifies three places where that can happen: the range given to Rand_int, the size given to the vector, and the loop bounds that control how many draws are made. Checking each one in order narrows the search quickly. The most common subtle failure is the mismatch between the range and the vector size, which is an off-by-one error.

index 0valid slotindex 9valid slotindex 10no slot existsone past the last slot
With a generator for the closed range from 1 to 10 and a vector of size 10, the valid indices run from 0 to 9. The value 10 is returned by the generator but has no slot when used as an index.
  • Common mistake

    Sizing the vector to the largest value instead of largest value plus one

    What it looks like
    vector<int> histogram(6); with Rand_int rnd(0, 6) and histogram[rnd()] incremented
    Why it fails

    The source uses max plus one slots so that every index from 0 through max has a place to be counted. With six slots, index 6 does not exist.

    Do this instead

    Create the vector with max + 1 slots so the largest possible index is still valid.

  • Common mistake

    Assuming the generator's range and the vector's indices are the same thing by coincidence

    What it looks like

    A generator for [1,10] used directly as an index into a size-10 vector, where the value 10 would need index 10

    Why it fails

    Indices in a vector start at 0. A closed range that starts at 1 and ends at 10 does not line up with slots 0 through 9 unless you shift or resize the vector.

    Do this instead

    Match the vector size and offset to the range you actually generate, and check the endpoints by tracing one value, such as 10, through the counting line.

  • Common mistake

    Assuming a histogram with slightly unequal bars is wrong

    Why it fails

    The source says a uniform distribution shows roughly similar counts with statistical variation, not exactly identical counts, because 200 draws is a finite sample.

    Do this instead

    Judge fairness by whether the bars are roughly similar across many draws, not by expecting exact equality.

Practice: Checking the Range and the Slots

MEDIUM

A program creates Rand_int rnd(1, 6) to simulate a die and uses std::vector<int> histogram(6) with the counting line ++histogram[rnd()] for 600 draws. Identify the first value that will cause a problem, and explain which slot it should land in if the program were corrected.

Hints
  • Compare the largest value the generator can return with the largest valid index of a vector of size 6.
  • Indices in a vector start at 0, so a vector of size 6 has indices 0 through 5.
  • A corrected version can use a vector of size 7 with the value 6 landing at index 6, or shift each value down by one before indexing.

Key Takeaways

  • Rand_int stores a range in its constructor and returns one random integer per call through operator(), so the range you specify is the range that comes out.
  • The default_random_engine supplies raw randomness, and the uniform_int_distribution makes every value in the range equally likely; both are private and reached only through the public operator.
  • A histogram vector with max plus one slots counts how often each value from 0 through max is generated, with the counting line ++histogram[rnd()] doing the work on each draw.
  • A uniform distribution shows roughly similar counts with statistical variation, not exactly identical counts, so judge fairness across many draws.
  • Off-by-one errors usually hide in the mismatch between the generator's range, the vector size, and the loop bounds; trace a single value such as the largest possible one through the counting line to find them.

Related concepts

More concepts starting with V