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.
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.
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.
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.
#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;
}
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.
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.std::uniform_int_distribution, as generate_password does here, which corrects for exactly this bias.std::mt19937 with a fixed literal seed in the real, shipped program — every run would generate the exact same “random” password.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.
#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
What it expects
$ ./genpw 20What it returns
$bYqzeT7&%jS6 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.
g++ -std=c++20 -o genpw main.cpp PasswordGenerator.cpp && ./genpw 20Run 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.
$ ./genpw 20
#=cWX((6d@tfpK_YeG!Pmain.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.PasswordGenerator.tpp is actually being #include’d by the header, not compiled as a separate .cpp file.// 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.' }
}
}
- 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.)
- Add a command-line flag per character set.
--no-symbols,--digits-only, and so on. (Teaches: simple argument parsing without a library.) - 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.)
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).