Skip to content
ilqvyaPublic

About

Random for modern C++ with convenient API

Topics

Resources

Stars

952 stars

Watchers

28 watching

Forks

Latest commit

 

History

532 Commits

Folders and files

Repository files navigation

Random for modern C++ with convenient API

CI Coverage Status Coverity Scan Build Status

Design goals

There are a few ways to work with random in C++:

  • C style
  srand( time(NULL) ); // seed with time since epoch
  auto random_number = (rand() % 9) + 1; // get a pseudo-random integer between 1 and 9
  • C++11 style
  std::random_device random_device; // create object for seeding
  std::mt19937 engine{random_device()}; // create engine and seed it
  std::uniform_int_distribution<> dist(1,9); // create distribution for integers with [1; 9] range
  auto random_number = dist(engine); // finally get a pseudo-random integer number
  • Problems:
    • Must specify a seed manually
    • Must choose, create, and manage a chain of various objects like engines and distributions
    • mt19937 uses ~5000 bytes of internal state for each instance (which hurts performance if created repeatedly)
    • Cumbersome and not intuitive to use
  • ilqvya random style
  // auto seeded
  auto random_number = Random::get(1, 9); // invoke 'get' method to generate a pseudo-random integer in [1; 9] range
  // yep, that's all.
  • Advantages:
    • Intuitive syntax. You can do almost everything with random via a simple 'get' method: generating numbers, bools, random elements from a given container or set, or using custom distributions.
    • Trivial integration. All code consists of a single header file random.hpp. That's it. No compiled library, no subproject, no external dependencies, no complex build system. Written in vanilla C++11 and compatible with modern C++.
    • Usability. Available generator types:
      • random (or random_thread_local): static methods and thread_local internal state. Fully thread-safe default!
      • random_static: static methods and static internal state. Fast, but not thread-safe across concurrent threads.
      • random_local: non-static member methods and local member state. Can be created on the stack at local scope.
    • Compile-Time Safety. Temporary rvalue containers and arrays passed to iterator-returning overloads are deleted (= delete) at compile time to prevent dangling iterators. String literals return by value without the null terminator, and empty array access is caught at compile time.
    • Zero-Allocation Sampling & Performance. Features immediate return fast-paths for zero-gap ranges (from == to), container reservation (reserve_if_supported), and optimal sampling algorithms without replacement.
    • C++20 Modules Support. Can be consumed either as a single header file (ilqvya/random.hpp) for C++11–C++26 or imported as a native C++20 module (import ilqvya.random;) for zero macro leakage, clean encapsulation, and faster compilation times.
    • Next-Gen Performance. Uses std::philox4x64 as the default engine in C++26 (when available), delivering fast counter-based random number generation with minimal 32-byte state and full 256-bit entropy seeding, while seamlessly falling back to std::mt19937 in C++11–C++23.

Supported compilers

  • GCC 4.9+ (and later)
  • Clang 3.7+ (and later)
  • Microsoft Visual C++ 2015+ (and later)

Integration

CMake

  • Header-only (as subproject or external package):
find_package(ilqvya_random REQUIRED)
target_link_libraries(${TARGET} ilqvya::random)
  • C++20 Module (import ilqvya.random;): Enable -DRandom_BuildModules=ON during CMake configuration (requires CMake 3.28+ and Ninja or Visual Studio generator):
find_package(ilqvya_random REQUIRED)
target_link_libraries(${TARGET} ilqvya::random_module)

Manually

The single required source file, random.hpp, is located in the include/ilqvya directory. The module interface ilqvya_random.cppm is in src/.

Usage

  • Header-only (C++11 through C++26): Add the header to your source file:
#include "ilqvya/random.hpp"

// Both ilqvya::random and ilqvya::Random work directly
auto val = ilqvya::Random::get(1, 6);
  • C++20 Module (import ilqvya.random;): If your toolchain supports C++20 modules, you can import the module directly:
import ilqvya.random;

int main() {
    auto dice = ilqvya::random::get(1, 6);
    auto token = ilqvya::random::get_string(16);
    auto sampled = ilqvya::random::sample(population, 3);
}

Ensure C++11 or newer is enabled in your compiler settings (e.g., -std=c++11 for GCC and Clang, or -std=c++20 when using modules).

Five-minute tutorial

Note

All tutorial examples below work identically with either #include <ilqvya/random.hpp> or C++20 import ilqvya.random;. Both ilqvya::Random and ilqvya::random are available out of the box in both modes!

Number range

Returns a pseudo-random number in a [first; second] range.

auto val = Random::get(-1, 1); // decltype(val) is int
// specify explicit type
auto val = Random::get<uint8_t>(-1, 1); // decltype(val) is uint8_t
// You can also specify range from greater to lower
auto val = Random::get(1.l, -1.l); // decltype(val) is long double
// std::byte support (C++17+)
auto b = Random::get<std::byte>(); // random byte in [0, 255]
auto bounded_b = Random::get(std::byte{0}, std::byte{128});
auto val = Random::get(1.f, -1); // Error: implicit conversions are not allowed here.

Common type number range

Choose common type of two range arguments by std::common_type.

auto val = Random::get<Random::common>(1, 0.f); // decltype(val) is float
auto val = Random::get<Random::common>(0ul, 1ull); // decltype(val) is unsigned long long
auto val = Random::get<Random::common>(1.2l, 1.5f); // decltype(val) is long double
auto val = Random::get<Random::common>(1u, -1); // Error: prevents conversion between signed and unsigned without common type promotion.

Character range

Returns a pseudo-random character in a [first; second] range.

auto val = Random::get('a', 'z');
auto val = Random::get(L'㋐', L'㋾');
auto val = Random::get<wchar_t>();
auto val = Random::get(u'a', u'z'); // char16_t
auto val = Random::get(U'a', U'z'); // char32_t
auto val = Random::get<char32_t>(); // default limits

Bool & Coin Flip

Generates a random boolean with specified probability in [0.0, 1.0], or convenient Bernoulli aliases:

auto val = Random::get<bool>(0.7); // true with 70% probability
auto def = Random::get<bool>();    // true with 50% probability by default

// Fast Bernoulli probability alias & coin flip:
bool heads = Random::flip();               // 50% coin flip (alias for flip(0.5))
bool crit  = Random::chance(0.15);         // true with 15% probability in [0.0, 1.0]
bool lucky = Random::chance_pct(25.0);     // true with 25% percentage in [0.0, 100.0]

// Out of range probabilities throw std::invalid_argument:
// Random::chance(-0.1);
// Random::chance_pct(101.0);

Random value from std::initializer_list

Returns a random value from values in a std::initializer_list. Throws std::invalid_argument if the list is empty.

auto val = Random::get({1, 2, 3}); // val = 1 or 2 or 3

Random iterator

Returns a random iterator from an iterator range or container. Iterator must satisfy at least ForwardIterator. If std::distance(first, last) == 0, returns the 'last' iterator. If the container is empty, returns the std::end(container) iterator.

std::array<int, 3> array{ {1, 2, 3} };
  • Iterator range
auto randomIt = Random::get( array.begin(), array.end() );
  • Container
auto randomIt = Random::get( array );

Random element from array

Returns a pointer to a random element in a built-in array.

int array [] = {1, 2, 3};
auto randomPtr = Random::get( array );

Container of random values

Returns a container filled with random numbers. Any container with "begin", "end", and "insert" methods is supported:

auto vec = Random::get<std::vector>(1, 9, 5); // decltype(vec) is std::vector<int> with size = 5
// Note: "reserve" method is invoked automatically if available for performance

auto deq = Random::get<std::deque>(1, 9, 5); // decltype(deq) is std::deque<int> with size = 5

auto lst = Random::get<std::list>(1.0, 9.9, 10); // decltype(lst) is std::list<double> with size = 10

auto mset = Random::get<std::multiset>(1.0, 9.9, 10); // decltype(mset) is std::multiset<double> with size = 10

auto arr = Random::get<std::array, 5>('0', '9'); // decltype(arr) is std::array<char, 5>
// Warning: Returning arrays with large size could be inefficient

auto vec = Random::get<std::vector>(1l, 9ll, 5); // decltype(vec) is std::vector<long long> with size = 5

template<typename T>
class MyContainer
{
    iterator begin() {...}
    iterator end() {...}
    void insert(iterator after, T value) {...}
};

auto vec = Random::get<MyContainer>(1, 9, 5); // decltype(vec) is std::MyContainer<int> with size = 5

Multi-roll and batch numbers (structured bindings)

Generate a fixed-size std::array of random numbers with compile-time size N, perfect for unpacking with C++17 structured bindings:

// RGB color triple:
auto [r, g, b] = Random::get<std::array, 3>(0, 255);

// 3D coordinate:
auto [x, y, z] = Random::get_array<3>(-1.0f, 1.0f);

// Multi-roll dice:
auto dice = Random::get_array<6>(1, 6); // std::array<int, 6>

// Default limits:
auto arr = Random::get_array<4, int>(); // std::array<int, 4> with default limits

In-place fill

Fill an existing container, iterator range, or built-in array in-place with random values, avoiding reallocations:

// Container in-place fill:
std::vector<int> buffer(1024);
Random::fill(buffer, 0, 100);

// Iterator range fill:
Random::fill(buffer.begin(), buffer.begin() + 10, -5, 5);

// Built-in C-style array fill:
int raw[8];
Random::fill(raw, 1, 6);
Random::fill(raw); // default limits

Weighted random values

Selects a random element based on relative weights:

  • Initializer list of weighted values: Returns the selected value directly based on weights using direct zero-allocation selection:
    // Explicit return type with initializer list of {value, weight}
    auto val = Random::get<int>( { { 1, 10 }, { 2, 20 }, { 3, 70 } } ); // 10%, 20%, 70% probability
    auto str = Random::get<std::string>( { { "rare", 1.5 }, { "common", 98.5 } } );

    // Explicit weighted_result type
    using WR = Random_t::weighted_result<int, double>;
    auto chosen = Random::get( { WR{ 10, 0.25 }, WR{ 20, 0.75 } } );
  • Supports both integer weights (int, unsigned, size_t) and floating-point weights (float, double).

  • Throws std::invalid_argument if the list is empty or if any weight is negative.

  • If all weights are 0, safely returns the first element.

  • weighted_result provides constexpr operator== and operator!=.

  • Variadic syntax (convenient for 2 to 10 weighted items):

    auto val = Random::get<int>( { 1, 3 }, { 4, 4 }, { 10, 5 } ); // returns chosen int based on weights 3, 4, 5
    auto f   = Random::get<float>( { 1.1f, 1 }, { 1.2f, 2 } );
  • Containers of pairs or map-like containers (returns iterator):
    std::unordered_map<std::string, unsigned long> nonzero_ulong_umap = { { "Orange", 1ul }, { "Apple", 2ul }, { "Banana", 3ul } };
    std::map<std::string, double> nonzero_double_map = { { "Orange", 1.0 }, { "Apple", 2.0 }, { "Banana", 3.0 } };
    std::vector<std::pair<std::string, int>> nonzero_vec = { { "Orange", 1 }, { "Apple", 2 }, { "Banana", 3 } };

    auto it1 = Random::get<Random_t::weight>( nonzero_ulong_umap );
    auto it2 = Random::get<Random_t::weight>( nonzero_double_map );
    auto it3 = Random::get<Random_t::weight>( nonzero_vec );
  • Returns mutable iterator for non-const containers, and const iterator for const containers.
  • Passing temporary rvalue containers is disabled at compile time (= delete) to prevent dangling iterators.

Sample without replacement

Samples $N$ unique elements from a population without replacement (guaranteed no duplicates from population). Available across all C++ standards (C++11 through C++26):

  • Containers and Ranges (returns container with sampled elements):
std::vector<int> population = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Returns std::vector<int> containing 4 uniquely sampled elements
auto sample_vec = Random::sample( population, 4 );

// Specify destination container type (e.g. std::deque, std::set, std::unordered_set)
auto sample_deq  = Random::sample<std::deque>( population, 3 );
auto sample_set  = Random::sample<std::set>( population, 4 );
auto sample_uset = Random::sample<std::unordered_set>( population, 4 );
  • Built-in Arrays:
int arr[] = { 10, 20, 30, 40, 50 };
auto sampled = Random::sample( arr, 3 ); // std::vector<int> with 3 unique elements
  • String Literals and Random String Sampling:
// Samples unique characters excluding the null terminator '\0', returns std::string
auto chars = Random::sample( "abcdefghij", 4 );

// sample_string helper for character ranges or string literals
std::string text = "ABCDEFGHIJ";
auto sampled_str = Random::sample_string( text, 3 );
  • Iterator Ranges:
std::vector<int> out;
Random::sample( population.begin( ), population.end( ), std::back_inserter( out ), 4 );

If n <= 0, returns an empty container or leaves output unmodified.

Choice with replacement

Selects $N$ random elements from a population with replacement (elements can be selected multiple times):

std::vector<int> population = { 1, 2, 3, 4, 5 };

// Returns std::vector<int> containing 10 elements chosen with replacement:
auto chosen = Random::choice( population, 10 );

// Specify destination container type:
auto chosen_deq = Random::choice<std::deque>( population, 5 );

// String literals and choice_string:
auto seq = Random::choice( "ACGT", 16 ); // std::string of 16 DNA bases
auto str = Random::choice_string( std::string{ "0123456789" }, 6 );

// Built-in arrays:
int arr[] = { 10, 20, 30 };
auto choices = Random::choice( arr, 5 );

// Iterator ranges:
std::vector<int> out;
Random::choice( population.begin(), population.end(), std::back_inserter( out ), 4 );

Enum random values (C++26 Reflection)

Returns a random enumerator from any enum or enum class (requires C++26 with reflection support, e.g. -std=c++26 -freflection):

enum class Color { Red, Green, Blue };
auto color = Random::get<Color>(); // Returns randomly chosen Color

enum Status { Disconnected = 0, Connecting = 10, Connected = 100 };
auto status = Random::get<Status>(); // Returns randomly chosen Status

Shuffle & std::span views

Reorders the elements in a given range, container, or built-in array ref:

  • Iterator range:
std::vector<int> vec{ 1, 2, 3, 4, 5 };
Random::shuffle( vec.begin( ), vec.end( ) );
  • Container & C++20 std::span / range views:
Random::shuffle( vec );

// C++20 std::span and subranges are supported directly:
std::span<int> sp{ vec };
Random::shuffle( sp );
  • Built-in array:
int array[] = { 1, 2, 3, 4, 5 };
Random::shuffle( array ); // supported for C-style arrays (rvalue arrays deleted for safety)

Random string

Generates a random std::string of specified length:

  • Default character range (printable characters ' ' to '~', avoiding embedded null bytes and non-printable control characters):
auto s  = Random::get_string( 16 );          // 16 random printable ASCII characters
auto ws = Random::get_string<wchar_t>( 16 ); // 16 random printable wide characters
  • From a character range [from, to]:
auto token  = Random::get_string( 16, 'a', 'z' ); // 16 random lowercase letters
auto digits = Random::get_string( 6, '0', '9' );  // 6 random digits
  • From a custom charset (string literal, std::string, or container of characters):
auto hex = Random::get_string( 32, "0123456789abcdef" ); // 32 random hex characters (excludes '\0')
auto key = Random::get_string( 16, std::string{ "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" } );

Throws std::invalid_argument if charset container is empty or range is invalid. Empty string literals fail at compile time.

Hex & UUID / GUID Generator

Generate hexadecimal strings and standard UUIDs / GUIDs or MongoDB ObjectIDs:

Warning

Not Cryptographically Secure: Generated values (strings, hex tokens, UUIDs) are driven by pseudo-random engines (std::mt19937 or std::philox4x64) and are not cryptographically secure (not a CSPRNG). mt19937 state is predictable from past output. Do not use them for cryptographic secrets, session IDs, authentication tokens, or password reset links.

// Hex strings:
auto hex32 = Random::get_hex(32);        // e.g. "a3f1c8..." (lowercase by default)
auto hex16 = Random::get_hex(16, true);  // e.g. "A3F1C8..." (uppercase)

// UUIDs (default is UUIDv4 - RFC 4122 random):
std::string uuid4 = Random::get_uuid(); 
// e.g. "123e4567-e89b-42d3-a456-426614174000"

// Specific UUID versions via ilqvya::uuid_version (RFC 4122 / RFC 9562 / MongoDB):
using ilqvya::uuid_version;
auto v4  = Random::get_uuid(uuid_version::v4);               // RFC 4122 random (default)
auto v1  = Random::get_uuid(uuid_version::v1);               // Gregorian time-based (RFC 4122)
auto v6  = Random::get_uuid(uuid_version::v6);               // Reordered Gregorian time-based (RFC 9562)
auto v7  = Random::get_uuid(uuid_version::v7);               // Unix epoch millisecond time-ordered (RFC 9562, monotonic)
auto nil = Random::get_uuid(uuid_version::nil);              // "00000000-0000-0000-0000-000000000000"
auto max = Random::get_uuid(uuid_version::max);              // "ffffffff-ffff-ffff-ffff-ffffffffffff"
auto oid = Random::get_uuid(uuid_version::mongo_object_id);  // 24 hex characters MongoDB ObjectID (timestamp + process random + counter)

Common Distributions

Direct helpers for non-uniform standard distributions:

// Normal (Gaussian) distribution:
auto norm = Random::normal(0.0, 1.0);       // mean = 0.0, stddev = 1.0 (default 0.0, 1.0)
auto nval = Random::normal();               // defaults to mean 0.0, stddev 1.0

// Exponential distribution:
auto exp_val = Random::exponential(1.5);    // lambda rate = 1.5 (default 1.0)

// Poisson distribution:
auto p_val = Random::poisson(4.0);          // mean = 4.0 (default 1.0)

Call operator (functor)

Random generator instances and static classes can be invoked directly using the function call operator operator():

auto num = Random( 1, 100 ); // same as Random::get( 1, 100 )

ilqvya::random_local rng;
auto val = rng( 1.0, 5.0 );  // same as rng.get( 1.0, 5.0 )

Custom distribution

Returns the result from operator() of a distribution using the internal engine:

  • Template argument
// 1.f and 2.f will be forwarded to std::gamma_distribution constructor
auto result = Random::get<std::gamma_distribution<>>( 1.f, 2.f );
  • Argument by reference or temporary
std::gamma_distribution<> gamma{ 1.f, 2.f };
auto result = Random::get( gamma ); // returns result of gamma.operator()( engine )

// Or pass temporary distribution directly:
auto inline_result = Random::get( std::uniform_int_distribution<int>{ 1, 6 } );

Custom Seeder

Specify a seed with which the random engine will be initialized at construction time:

  • Number
struct MySeeder {
    unsigned operator() () {
        return 42u;
    }
};
    
// Seeded by 42
using Random = ilqvya::basic_random_static<std::mt19937, MySeeder>;
  • Seed sequence

Because std::seed_seq is non-copyable, the 'random' library destroys the seeder instance after engine seeding, so returning the seed sequence by reference is safe:

struct MySeeder {
    // std::seed_seq isn't copyable
    std::seed_seq& operator() () {
        return seed_seq_;
    }
    std::seed_seq seed_seq_{ { 1, 2, 3, 4, 5 } };
};
    
// Seeded by seed_seq_ from MySeeder
using Random = ilqvya::basic_random_static<std::mt19937, MySeeder>;
  • Reseed

Seeds the internal random engine using a newly created Seeder instance:

Random::reseed( );

Thread local random

Uses static method API with thread_local storage, which is fully thread-safe (and available as default alias ilqvya::random):

using Random = ilqvya::random; // or ilqvya::random_thread_local

// use in the same way as random_static. Thread safe
std::thread first{ [ ] { Random::get( ); } };
std::thread second{ [ ] { Random::get( ); } };

Local random

Uses member method API with local member storage, created on the stack at local scope:

#include "ilqvya/random.hpp"
// Or when using C++20 modules:
// import ilqvya.random;

using Random_t = ilqvya::random_local;

int main( ) {
  Random_t localRandom{ }; // Construct on the stack (auto-seeded)
  
  // Custom initialization:
  Random_t seeded{ 42u };                 // Seeded with unsigned integer
  std::seed_seq sseq{ 1, 2, 3, 4, 5 };
  Random_t from_seq{ sseq };              // Seeded with std::seed_seq
  std::mt19937 custom_eng{ 12345 };
  Random_t from_eng{ custom_eng };        // Initialized from an existing engine

  // Access via dot operator or call operator
  auto val = localRandom.get( -10, 10 );
  auto roll = localRandom( 1, 6 );

  // Generators can be compared for identical engine state
  if ( seeded == from_seq ) { /* ... */ }
  
} // Destroy localRandom and free stack memory

Stream operators

In addition to serialize and deserialize, generators support standard stream insertion (<<) and extraction (>>) operators:

std::stringstream ss;
ss << Random; // Serializes internal engine state into stream

ss >> Random; // Restores internal engine state from stream

Seeding

ref

Sets a new seed for the internal random engine:

Random::seed( 10 ); // 10 is new seed number

std::seed_seq sseq{ 1, 2, 3 };
Random::seed( sseq ); // use seed sequence here

Min value

ref

Returns the minimum value potentially generated by the internal random-number engine:

auto minVal = Random::min( );

Max value

ref

Returns the maximum value potentially generated by the internal random-number engine:

auto maxVal = Random::max( );

'get' without arguments

ref

Returns the random number in [ Random::min( ), Random::max( ) ] range:

auto val = Random::get( );
// val is random number in [ Random::min( ), Random::max( ) ] range

engine

Returns a reference to the internal engine:

auto& engine = Random::engine( );
const auto& const_engine = Random::engine( );
std::sample(itBeg, itEnd, std::back_inserter(out), 5, Random::engine( ));

Get engine

Returns a copy of the internal engine:

auto engine = Random::get_engine( );

Discard

ref

Advances the internal engine's state by a specified amount. Equivalent to calling Random::get() N times and discarding the result.

Random::discard( 500 );

Is equal

ref

Compares the internal pseudo-random number engine with another engine or generator:

Random::engine_type otherEngine;
bool isSame = Random::is_equal( otherEngine );

ilqvya::random_local rng1, rng2;
if ( rng1 == rng2 ) { /* ... */ }
if ( rng1 != rng2 ) { /* ... */ }

Serialize

ref

Serializes the internal state of the pseudo-random number engine as a sequence of decimal numbers separated by spaces, and inserts it into the output stream. The fill character and formatting flags of the stream are ignored and unaffected.

std::stringstream strStream;
Random::serialize( strStream ); // strStream now contains the internal state of the Random engine

Deserialize

ref

Restores the internal state of the pseudo-random number engine from a serialized representation created by an earlier call to 'serialize' using a stream with the same imbued locale, CharT, and Traits. If the input cannot be deserialized, the internal engine is left unchanged and failbit is set on the input stream.

std::stringstream strStream;
Random::serialize( strStream );

// ...

Random::deserialize( strStream ); // Restore internal state of the Random engine

About

Random for modern C++ with convenient API

Topics

Resources

Stars

952 stars

Watchers

28 watching

Forks

Releases

Packages

Used by

Contributors

Languages