← thecodex.expert · The Codex Family of Knowledge
Tier 0 · Absolute Beginner · C++ Project

Password Generator

Generate a random password from a configurable mix of lowercase, uppercase, digits, and symbols. The interesting design problem: how do you test something whose entire job is to be unpredictable?

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a tool that creates a strong password: a random mix of letters, numbers, and symbols, of a length the user chooses. It teaches building a string from random choices — and an important lesson about which randomness is safe for security.

Where this shows up: password managers, generating API keys, session tokens, temporary access codes, unique IDs. Any time software needs something unpredictable that an attacker cannot guess.

2 How to Think About It

Two pieces: building the pool of allowed characters from a bitmask, and drawing length random characters from that pool. The randomness itself is the part that needs a deliberate design choice to stay testable.

The plan — in plain English
1. Build the character pool from which set flags (CS_LOWER, CS_UPPER, CS_DIGITS, CS_SYMBOLS) are set. → 2. Draw length random indices into that pool. → 3. Assemble the characters at those indices into the final password.

Build character pool

Ask for length

Pick that many random chars

Join into a password

Show the password

3 The Build — explained part by part

Here is the complete generator, split across a header, a template implementation file (.tpp), a source file, and main.cpp. The template is the one piece worth reading closely before the rest.

C++PasswordGenerator.hpp / PasswordGenerator.tpp / PasswordGenerator.cpp / main.cpp
#pragma once
#include <string>

enum CharSet {
    CS_LOWER = 1,
    CS_UPPER = 2,
    CS_DIGITS = 4,
    CS_SYMBOLS = 8,
};

// Builds the pool of characters selected by `mask` (an OR of CharSet
// values). Returns the empty string if `mask` selects nothing.
std::string build_pool(int mask);

// Generates a random password of `length` characters drawn from the pool
// selected by `mask`, using `rng` as the source of randomness -- a
// template so the real program can pass a genuinely secure
// std::mt19937 seeded from std::random_device, while the tests pass a
// small seeded deterministic engine for reproducible output. Returns an
// empty string if the mask selects no characters or length is not
// positive.
template <typename RNG>
std::string generate_password(int length, int mask, RNG &rng);

#include "PasswordGenerator.tpp"

#pragma once
#include <random>

template <typename RNG>
std::string generate_password(int length, int mask, RNG &rng) {
    std::string pool = build_pool(mask);
    if (pool.empty() || length <= 0) return "";

    std::uniform_int_distribution<std::size_t> dist(0, pool.size() - 1);
    std::string out;
    out.reserve(static_cast<std::size_t>(length));
    for (int i = 0; i < length; i++) {
        out += pool[dist(rng)];
    }
    return out;
}

#include "PasswordGenerator.hpp"

std::string build_pool(int mask) {
    std::string pool;
    if (mask & CS_LOWER) pool += "abcdefghijklmnopqrstuvwxyz";
    if (mask & CS_UPPER) pool += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    if (mask & CS_DIGITS) pool += "0123456789";
    if (mask & CS_SYMBOLS) pool += "!@#$%^&*()-_=+";
    return pool; // empty if mask selected no character set at all
}

#include "PasswordGenerator.hpp"
#include <iostream>
#include <random>

int main(int argc, char **argv) {
    int length = argc > 1 ? std::stoi(argv[1]) : 16;
    int mask = CS_LOWER | CS_UPPER | CS_DIGITS | CS_SYMBOLS;

    // std::random_device is a real, non-deterministic entropy source on this
    // platform (confirmed by checking its .entropy() is non-zero), seeding a
    // std::mt19937 for the actual generation work. This is standard, portable
    // C++ -- no external library or platform-specific extension needed.
    std::random_device rd;
    std::mt19937 rng(rd());

    std::string password = generate_password(length, mask, rng);
    if (password.empty()) {
        std::cerr << "Could not generate a password (bad length or empty character set)\n";
        return 1;
    }
    std::cout << password << "\n";
    return 0;
}
⚠ No in-browser playground here
C++ compiles to a real, native binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once a C++17-or-newer compiler like g++ or clang++ is installed.
What each part does — in plain words
template <typename RNG> std::string generate_password(int length, int mask, RNG &rng) — the whole point of making this a template: the function does not care which random number generator it receives, only that it behaves like one (callable, returning an integer in range). That is what lets main.cpp pass a std::mt19937 seeded from real entropy for an actual unpredictable password, while the tests below pass std::mt19937 rng(42) — the same fixed seed every run, so the “random” output is perfectly reproducible and testable. C’s version of this project needed a build-time #ifdef TESTING to swap in a fixed-seed source; C++ templates make that swap a normal function parameter instead.

PasswordGenerator.tpp, #include’d at the bottom of the header — templates are compiled per call site, so their full definition (not just a declaration) has to be visible everywhere they are used. The conventional way to keep a template’s implementation out of the header’s main reading flow while still satisfying that rule is a separate .tpp file, included at the header’s end.

std::random_device rd; std::mt19937 rng(rd());, only in main.cpp — confirmed on this platform, by checking rd.entropy() is non-zero in a standalone probe program, to be genuine non-deterministic entropy, not a disguised pseudo-random fallback. That makes this C++ version’s randomness story simpler than C’s: C needed the glibc-specific arc4random_uniform for a comparable guarantee, while C++’s standard <random> header does it portably.
Common mistakes — and how to avoid them
✗ Using rand() % pool.size() to pick a character — besides being lower quality and not seeded well by default, % on a small pool introduces a slight, measurable bias toward the lowest indices.
✓ Use std::uniform_int_distribution, as generate_password does here, which corrects for exactly this bias.
✗ Seeding std::mt19937 with a fixed literal seed in the real, shipped program — every run would generate the exact same “random” password.
✓ Reserve the fixed seed for tests only, as this project does; the real main.cpp must seed from std::random_device.

4 Test & Prove Each Part

Six checks: pool construction for each character-set combination, length correctness, that every character actually comes from the requested pool, and that an empty pool or non-positive length fails cleanly instead of producing garbage. Each test passes its own fixed-seed std::mt19937 so the results are exactly reproducible — the same hand-written assert() harness used throughout this project, in place of the unreachable Catch2 or GoogleTest.

build_pool includes only the character sets that were selected
An empty selection (mask 0) builds an empty pool
A generated password has exactly the requested length
Every character in the output comes from the selected pool, never outside it
An empty character set fails cleanly (empty string), not a crash
A non-positive length produces an empty string
C++test_PasswordGenerator.cpp
#include "PasswordGenerator.hpp"
#include <cassert>
#include <iostream>
#include <random>

#define RUN(name) do { name(); std::cout << "PASS: " << #name << "\n"; } while (0)

static void build_pool_includes_only_selected_sets() {
    assert(build_pool(CS_LOWER) == "abcdefghijklmnopqrstuvwxyz");
    assert(build_pool(CS_DIGITS) == "0123456789");
    std::string both = build_pool(CS_LOWER | CS_DIGITS);
    assert(both.find("a") != std::string::npos);
    assert(both.find("5") != std::string::npos);
    assert(both.find("A") == std::string::npos);
}

static void an_empty_selection_builds_an_empty_pool() {
    assert(build_pool(0).empty());
}

static void generated_password_has_the_requested_length() {
    std::mt19937 rng(42); // fixed seed -- deterministic and reproducible for a test
    std::string pw = generate_password(12, CS_LOWER | CS_DIGITS, rng);
    assert(pw.size() == 12);
}

static void every_character_comes_from_the_selected_pool() {
    std::mt19937 rng(7);
    std::string pool = build_pool(CS_UPPER | CS_SYMBOLS);
    std::string pw = generate_password(50, CS_UPPER | CS_SYMBOLS, rng);
    for (char ch : pw) {
        assert(pool.find(ch) != std::string::npos);
    }
}

static void an_empty_character_set_fails_cleanly_instead_of_crashing() {
    std::mt19937 rng(1);
    std::string pw = generate_password(10, 0, rng);
    assert(pw.empty()); // no character set selected -> no password, not garbage
}

static void a_non_positive_length_produces_an_empty_string() {
    std::mt19937 rng(1);
    assert(generate_password(0, CS_LOWER, rng).empty());
    assert(generate_password(-5, CS_LOWER, rng).empty());
}

int main() {
    RUN(build_pool_includes_only_selected_sets);
    RUN(an_empty_selection_builds_an_empty_pool);
    RUN(generated_password_has_the_requested_length);
    RUN(every_character_comes_from_the_selected_pool);
    RUN(an_empty_character_set_fails_cleanly_instead_of_crashing);
    RUN(a_non_positive_length_produces_an_empty_string);
    std::cout << "All tests passed.\n";
    return 0;
}

Compile and run with g++ -std=c++20 -o test_run PasswordGenerator.cpp test_PasswordGenerator.cpp && ./test_run. Note that PasswordGenerator.tpp needs no separate mention on the compile line — it is pulled in automatically by the #include at the bottom of the header.

5 The Interface

INPUTINPUTan optional length argument (default 16)
What it expects
$ ./genpw 20
OUTPUTOUTPUTa random password using all four character sets
What it returns
$bYqzeT7&%jS

6 Run It & Automate It

Save the code as PasswordGenerator.hpp / PasswordGenerator.tpp / PasswordGenerator.cpp / main.cpp and compile it with g++ — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.

Run it locally
g++ -std=c++20 -o genpw main.cpp PasswordGenerator.cpp && ./genpw 20
Run it a few times in a row — a different password every time is the whole point.

A CI tool like Jenkins runs the same compile-then-test-then-check-for-leaks steps automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
$ ./genpw 20
#=cWX((6d@tfpK_YeG!P
If it breaks — how to fix it
🚨 Could not generate a password (bad length or empty character set)
This is main.cpp’s own check for an empty generate_password result — either the requested length was not positive, or mask selected no character set at all.
🚨 The test file will not compile with an “undefined reference” to generate_password.
Template functions must be fully defined (not just declared) in every translation unit that calls them — make sure PasswordGenerator.tpp is actually being #include’d by the header, not compiled as a separate .cpp file.
GroovyJenkinsfile
// Jenkinsfile — compiles, tests, and checks for leaks on every change.
pipeline {
    agent any

    stages {
        stage('Get the code') {
            // download the latest code
            steps { checkout scm }
        }
        stage('Compile') {
            steps {
                // confirm a compiler is installed
                sh 'g++ --version'
                // compile with strict warnings on
                sh 'g++ -std=c++20 -Wall -Wextra -o app *.cpp'
            }
        }
        stage('Run the tests') {
            steps {
                // prints PASS/FAIL, exits non-zero on failure
                sh './app'
            }
        }
        stage('Check for memory leaks') {
            steps {
                // fails the build on any leak or invalid access
                sh 'valgrind --error-exitcode=1 --leak-check=full ./app'
            }
        }
    }

    post {
        success { echo 'All tests passed, no leaks found.' }
        failure { echo 'A test or Valgrind check failed — see above.' }
    }
}
🎯 Try this next — make it yours
  1. Guarantee at least one character from each selected set. A naive draw can (rarely) miss a whole category. (Teaches: a post-generation check-and-retry, or building the password from guaranteed slots plus random fill.)
  2. Add a command-line flag per character set. --no-symbols, --digits-only, and so on. (Teaches: simple argument parsing without a library.)
  3. Estimate and print the password’s entropy in bits. length * log2(pool.size()). (Teaches: connecting the code back to why a bigger pool and a longer password both matter.)
What you learned
You learned to make a template function generic over “anything that behaves like a random number generator,” letting production code and tests share one implementation while using different, appropriately-seeded engines, why std::uniform_int_distribution avoids the bias of % size, and why std::random_device makes C++’s secure-randomness story more portable than C’s. Related: Templates and the STL, Modern C++ (C++11–C++23).