15 Odds and Ends

You have come a long way. You can write programs with variables, control flow, functions, classes, containers, and file I/O. But there are practical gaps that come up in real programs. What if you need to terminate a program from deep inside a nested function call? What if you need to call a C library from C++? What if you need to convert between types safely, measure how long something takes, or generate random numbers? This chapter covers those remaining topics: exit() for program termination, extern "C" for C interoperability, C++ casting operators, the <chrono> time library, the <random> library, the <cmath> math functions, a tour of undefined behavior, and a first look at debuggers.

15.1 exit()

You already know that return 0; in main() ends the program successfully. But what if you need to stop the program from deep inside a function, not just from main()?

The exit() function, declared in <cstdlib>, terminates the program immediately from anywhere. Its signature is:

void exit(int status);
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>

void load_config(const std::string& filename) {
    std::ifstream infile(filename);

    if (!infile) {
        std::cerr << "Fatal: could not open " << filename
                  << std::endl;
        exit(EXIT_FAILURE);
    }

    std::cout << "Config loaded." << std::endl;
}

int main() {
    load_config("settings.cfg");
    std::cout << "Program running..." << std::endl;

    return EXIT_SUCCESS;
}

If settings.cfg does not exist, the program prints an error and stops immediately. The line “Program running…” never executes.

EXIT_SUCCESS and EXIT_FAILURE are constants defined in <cstdlib>. EXIT_SUCCESS is typically 0 and EXIT_FAILURE is typically 1, but using the named constants makes your intent clearer.

15.1.1 exit() vs return

When you call return from main(), the program exits in a controlled way — local variables in main() are cleaned up, destructors are called. exit() also performs cleanup: it flushes output streams and calls functions registered with atexit(). The signature of atexit() is:

int atexit(void (*func)());

However, local variables on the stack do not have their destructors called when exit() is used.

Tip: Prefer return from main() when possible. Use exit() when you need to terminate from a function deep in the call stack and returning an error code all the way up to main() would be impractical.

Trap: Because exit() does not call destructors for local variables on the stack, resources managed by RAII (like file handles or smart pointers in local scope) may not be cleaned up properly. Use exit() sparingly and with awareness of this limitation.

15.2 extern “C”

C++ grew out of C, and there is a massive amount of existing C code in the world. Sometimes you need to call C functions from C++, or make C++ functions callable from C. The key to this is extern "C".

15.2.1 Name Mangling

When you write a function in C++, the compiler does not store the function name as-is in the compiled output. Instead, it mangles the name — it encodes the function name along with its parameter types into a unique symbol.

This is necessary because C++ supports function overloading. Consider:

void play(int track);
void play(const std::string& song);
void play(int track, bool repeat);

All three functions are named play, but they take different parameters. The compiler needs to tell them apart in the compiled output, so it might mangle them into something like _Z4playi, _Z4playRKSs, and _Z4playib. The exact mangled names depend on the compiler, but the point is that each overload gets a unique name.

C does not have function overloading, so C compilers do not mangle names. A C function called play is simply stored as play in the compiled output.

This creates a problem: if you try to call a C function from C++, the C++ compiler will look for a mangled name that does not exist.

15.2.2 Disabling Name Mangling

extern "C" tells the C++ compiler: “do not mangle this function name — use C-style naming.”

extern "C" void c_function();

Now the C++ compiler knows to look for c_function without mangling, matching what a C compiler would produce.

15.2.3 Calling C Functions from C++

Imagine you have a C library — say, a legacy compression routine — with a function you want to use. You would declare it with extern "C":

// Sketch: legacy_compress.c is compiled and linked separately.
#include <cstddef>

// Tell C++ that this function uses C naming conventions:
extern "C" {
    int legacy_compress(const char *input, std::size_t len,
                        char *output);
}

void demo() {
    char        buffer[64];
    const char *song    = "La copa de la vida";
    int         written = legacy_compress(song, 18, buffer);
    (void)written;   // pretend we did something with it
}

The extern "C" block tells the compiler that all functions declared inside it use C linkage. Picking a hypothetical name like legacy_compress (rather than something that already lives in the standard headers, like sqrt or abs) avoids any chance of colliding with the C++ overloads <cmath> and <cstdlib> already provide.

Tip: In practice, you rarely need to write extern "C" declarations yourself. Most C library headers already handle this. But understanding why it exists helps you debug linker errors when mixing C and C++ code.

15.2.4 Wrapping C Headers

If you write a header that needs to work in both C and C++ code, you can use a common pattern:

#ifdef __cplusplus
extern "C" {
#endif

void mi_funcion(int x);
int otra_funcion(const char* s);

#ifdef __cplusplus
}
#endif

__cplusplus is a macro that is defined only when compiling with a C++ compiler. When compiled as C++, the functions get extern "C" linkage. When compiled as C, the extern "C" parts are skipped entirely because C does not understand that syntax.

Wut: extern "C" does not mean “compile this as C code.” The code inside extern "C" is still C++ — you can use C++ features. It only affects how the function name is stored in the compiled output.

15.3 Numbers and Casting

15.3.1 Everything is a Number

To the CPU, there are no strings, no classes, no booleans. There are only numbers — sequences of bits stored in memory. The types you use in C++ tell the compiler how to interpret those bits.

An int is a number you want to do arithmetic with. A char is also a number — just a smaller one. When you write 'A', the compiler stores the number 65. When you write '0', it stores 48.

#include <iostream>

int main() {
    char letter = 'A';

    std::cout << "As char: " << letter << std::endl;
    std::cout << "As int:  " << static_cast<int>(letter) << std::endl;
    std::cout << "'A' + 1 = " << static_cast<char>(letter + 1)
              << std::endl;

    return 0;
}

Output:

As char: A
As int:  65
'A' + 1 = B

The same bits that represent the character 'A' also represent the integer 65. The type is just a label that tells the compiler what to do with the number.

15.3.2 Bit Widths and Ranges

Different types use different numbers of bits, which determines the range of values they can hold.

Type Bits Minimum Maximum
int8_t 8 -128 127
uint8_t 8 0 255
int16_t 16 -32,768 32,767
uint16_t 16 0 65,535
int32_t 32 -2,147,483,648 2,147,483,647
uint32_t 32 0 4,294,967,295

These fixed-width types from <cstdint> guarantee exactly how many bits they use. The regular int is at least 16 bits but usually 32 bits on modern systems.

Trap: If you store a value too large for a type, it wraps around. For uint8_t, 255 + 1 becomes 0. For int8_t, 127 + 1 becomes -128. This is a common source of subtle bugs.

15.3.3 C++ Casts

Sometimes you need to convert a value from one type to another. This is called casting. C++ provides four named cast operators that are safer and more expressive than the old C-style cast.

15.3.3.1 static_cast

static_cast is the most common cast. Its syntax is:

static_cast<new_type>(expression)

Use it for well-defined, compile-time conversions between related types.

double pi = 3.14159;
int truncated = static_cast<int>(pi);  // 3 --- decimal part is lost

int score = 98;
double pct = static_cast<double>(score) / 100;  // 0.98

This is the cast you will use most often. It handles numeric conversions, conversions between related pointer types in a class hierarchy, and other conversions the compiler can verify at compile time.

15.3.3.2 dynamic_cast

dynamic_cast is used for safe downcasting in class hierarchies with virtual functions. Its syntax is:

dynamic_cast<new_type>(expression)

It checks at runtime whether the cast is valid.

#include <iostream>

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void special() { std::cout << "Do you believe?" << std::endl; }
};

int main() {
    Base* bp = new Derived();
    Derived* dp = dynamic_cast<Derived*>(bp);

    if (dp != nullptr) {
        dp->special();
    } else {
        std::cout << "Cast failed" << std::endl;
    }

    delete bp;
    return 0;
}

If the object pointed to by bp is not actually a Derived, dynamic_cast returns nullptr instead of producing undefined behavior.

Tip: dynamic_cast only works with polymorphic types — classes that have at least one virtual function. If you have not studied inheritance yet, just know that this cast exists for safely converting between related class types at runtime. Gorgo Continuing C++ covers inheritance, virtual functions, and dynamic_cast in depth in chapter 1.

15.3.3.3 const_cast

const_cast adds or removes const from a pointer or reference. Its syntax is:

const_cast<new_type>(expression)

This is rarely needed and usually a sign that something in the design should be reconsidered.

#include <iostream>

void legacy_print(char* s) {
    std::cout << s << std::endl;
}

int main() {
    const char* song = "Believe";
    // legacy_print(song);  // Error: invalid conversion to char*
    legacy_print(const_cast<char*>(song)); // Compiles, but be careful

    return 0;
}

The main legitimate use is interfacing with old C APIs that take non-const pointers but promise not to modify the data.

Trap: If you use const_cast to remove const and then actually modify the data, the behavior is undefined if the original object was declared as const. Only use const_cast when you are certain the data will not be modified.

15.3.3.4 reinterpret_cast

reinterpret_cast tells the compiler to treat the bits of one type as if they were another type entirely. Its syntax is:

reinterpret_cast<new_type>(expression)

This is the most dangerous cast and should be used rarely.

#include <iostream>
#include <cstdint>

int main() {
    int value = 42;
    uintptr_t addr = reinterpret_cast<uintptr_t>(&value);

    std::cout << "Address of value: " << addr << std::endl;

    return 0;
}

This cast performs no conversion — it just reinterprets the bit pattern. It is used in low-level code like memory allocators or hardware interfaces.

Wut: reinterpret_cast does not change the bits at all. A static_cast from float to int actually converts the value (3.14 becomes 3). A reinterpret_cast would take the raw bits of the float and pretend they are an int, producing a completely different and probably meaningless number.

15.3.4 Why C++ Casts Over C-Style Casts?

In C (and in C++), you can cast with the syntax (type)value:

double pi = 3.14;
int n = (int)pi;  // C-style cast --- works but not recommended in C++

C++ also allows a functional-style cast that looks like a function call:

int n = int(pi);  // functional-style cast --- same thing

Both forms are equivalent — (int)pi and int(pi) do exactly the same thing. Neither is recommended in new C++ code.

The problem with both forms is that they are blunt instruments. They can silently perform any of the four C++ casts, and you cannot tell which one just by looking at the code.

The C++ named casts are preferred because:

  • They express intent. static_cast says “this is a safe, well-defined conversion.” reinterpret_cast says “I know this is dangerous.”
  • They are searchable. You can search your codebase for reinterpret_cast to find all the dangerous casts. Good luck finding all the C-style casts with a search.
  • They are restrictive. Each C++ cast only allows certain conversions. A C-style cast can do anything, including things you did not intend.

Tip: Use static_cast for safe conversions between numeric types. Use dynamic_cast for safe downcasting in class hierarchies. Avoid const_cast and reinterpret_cast unless you have a very specific reason. Never use C-style casts in new C++ code.

15.4 Time

Programs often need to work with time — measuring how long something takes, pausing execution, or converting between time units. C++ provides the <chrono> library for this.

15.4.1 Measuring Elapsed Time

The most common use of <chrono> is measuring how long a piece of code takes to run. For this, std::chrono::steady_clock is the right clock because it never jumps forward or backward. The key functions are:

static time_point steady_clock::now();         // current time
Duration duration_cast<Duration>(duration d);  // convert time units
Rep duration::count() const;                   // the numeric value
void this_thread::sleep_for(duration d);       // pause execution
#include <chrono>
#include <iostream>
#include <thread>

int main() {
    auto start = std::chrono::steady_clock::now();

    // Simulate some work
    std::this_thread::sleep_for(std::chrono::milliseconds(150));

    auto end = std::chrono::steady_clock::now();
    auto elapsed =
        std::chrono::duration_cast<std::chrono::milliseconds>(
            end - start);

    std::cout << "That took " << elapsed.count() << " ms"
              << std::endl;

    return 0;
}

Output (approximately):

That took 150 ms

steady_clock::now() returns a time point. Subtracting two time points gives a duration. duration_cast converts that duration to the units you want — milliseconds, microseconds, seconds, etc.

15.4.2 Duration Arithmetic

Durations are type-safe. You cannot accidentally mix up seconds and milliseconds because they are different types. The library handles conversions automatically when it is safe.

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;

    seconds two_min = minutes(2);
    milliseconds half_sec = milliseconds(500);

    std::cout << "2 minutes = "
              << two_min.count()
              << " seconds" << std::endl;
    std::cout << "500 ms = "
              << duration_cast<seconds>(half_sec).count()
              << " seconds" << std::endl;

    auto mixed = seconds(3) + milliseconds(250);
    std::cout << "3s + 250ms = "
              << duration_cast<milliseconds>(mixed).count()
              << " ms" << std::endl;

    return 0;
}

Output:

2 minutes = 120 seconds
500 ms = 0 seconds
3s + 250ms = 3250 ms

Notice that converting 500 milliseconds to seconds gives 0, not 0.5. duration_cast truncates — it does not round. This is the same behavior as integer division.

Tip: Use std::chrono::steady_clock for measuring elapsed time. system_clock can jump forward or backward (e.g., when the system clock is adjusted), which would throw off your measurements.

Trap: duration_cast truncates toward zero. If you need to know that an operation took 1.7 seconds, cast to milliseconds (1700) rather than seconds (1).

15.5 Math (<cmath> and <numbers>)

Beyond the basic operators in Chapter 4, C++ inherits the C math library through the <cmath> header. You will need it whenever a program does anything more than addition and multiplication — distances, angles, growth curves, anything physical.

The most common functions:

double sqrt(double x);               // square root
double pow(double base, double exp); // base^exp
double abs(double x);                // |x|; int overload in <cstdlib>
double floor(double x);              // round toward negative infinity
double ceil(double x);               // round toward positive infinity
double round(double x);              // nearest, halves away from zero
double fmod(double x, double y);     // floating-point modulo
double sin(double x);                // trig functions take radians
double cos(double x);
double tan(double x);
double log(double x);                // natural log
double log2(double x);
double log10(double x);
double exp(double x);                // e^x

The trig functions take radians, not degrees — this is a constant source of beginner bugs.

For frequently used math constants, the C++20 <numbers> header provides typed compile-time values:

namespace std::numbers {
    constexpr double pi    = ...;    // pi_v<float>, pi_v<long double>
    constexpr double e     = ...;
    constexpr double sqrt2 = ...;
    constexpr double phi   = ...;    // golden ratio
    constexpr double ln2   = ...;
    constexpr double ln10  = ...;
}
#include <cmath>
#include <iostream>
#include <numbers>

int main() {
    double radius = 5.0;
    double area   = std::numbers::pi * std::pow(radius, 2);
    std::cout << "area: " << area << "\n";

    double degrees = 30;
    double radians = degrees * std::numbers::pi / 180.0;
    std::cout << "sin(30 deg): " << std::sin(radians) << "\n";

    std::cout << "sqrt(2):  " << std::sqrt(2.0)         << "\n";
    std::cout << "log2(8):  " << std::log2(8.0)         << "\n";
    std::cout << "floor:    " << std::floor(3.7)        << "\n";
    std::cout << "ceil:     " << std::ceil(3.2)         << "\n";
    std::cout << "round:    " << std::round(3.5)        << "\n";
    std::cout << "fmod:     " << std::fmod(7.5, 2.0)    << "\n";
    return 0;
}

Output:

area: 78.5398
sin(30 deg): 0.5
sqrt(2):  1.41421
log2(8):  3
floor:    3
ceil:     4
round:    4
fmod:     1.5

Tip: Reach for std::numbers::pi instead of 3.14159... typed by hand — the constant is precise to the full width of the type, and a future reader does not have to count the digits to confirm you didn’t typo one.

Trap: Trig functions take radians. To convert from degrees, multiply by std::numbers::pi / 180.0.

15.6 Undefined Behavior

You have seen scattered warnings throughout this book that some operation is undefined behavior (UB). It is worth pausing and treating UB as a topic in its own right, because it is the single biggest difference between C++ and most other languages.

When the C++ standard says “the behavior is undefined,” it means anything may happen. The compiler is not required to produce an error. The program is not required to crash. The result you got the first time is not the result you will get the next time. The compiler is allowed to assume your code never has UB and optimize accordingly — which sometimes erases checks you wrote on the assumption that UB would manifest as something visible.

Common sources of UB you have already met:

  • Reading uninitialized variables (Chapter 2): the value is whatever was previously in that memory.
  • Out-of-bounds array access (Chapters 2, 8): writing past the end of an array, dereferencing *end().
  • Signed integer overflow (Chapter 7): INT_MAX + 1 is UB. Unsigned overflow is not — it wraps modulo 2N.
  • Division by zero (Chapter 4).
  • Use-after-free (Chapter 13): touching memory through a pointer after delete.
  • Null pointer dereference (Chapter 13): *p when p is nullptr.
  • Iterator invalidation (Chapter 8): using an iterator after the container has been resized or reordered.
  • const_cast away const, then write: if the underlying object was actually const, modifying it through the cast is UB.

Why does the language allow this at all? The short answer is performance: when the compiler can assume that programs do not exhibit UB, it gets to skip a lot of runtime checks — bounds, signedness, alignment — that would otherwise slow every program down. The price is that programs which do exhibit UB can produce surprising results, and the surprises are often worse with optimization turned on.

Two practical defenses:

  • Compile with sanitizers during development. -fsanitize=address,undefined (with GCC or Clang) turns many forms of UB into immediate, loud crashes during testing.
  • Take warnings seriously. Many of the everyday UB sources — uninitialized reads, sign mismatches, missing returns — have warnings dedicated to them. Compiling with -Wall -Wextra -pedantic and treating new warnings as bugs catches most of them before they ship.

Wut: “Undefined behavior” is not a synonym for “crashes” or “produces wrong output.” A program with UB may appear to work for years, then break the day you upgrade your compiler, change optimization level, or run it on a different machine. The bug is real on day one — you just have not seen it yet.

15.7 Debuggers

When a program crashes, hangs, or quietly produces the wrong answer, you want to see what it is doing rather than guess. A debugger lets you pause the program at any point, inspect every variable, and step through one statement at a time. The two debuggers you will run into most often are gdb (GNU Debugger; ships with GCC) and lldb (the LLVM debugger; ships with Clang). They have different command names but the same mental model.

To debug effectively, compile with debug symbols and without optimization:

g++ -std=c++23 -g -O0 -o program program.cpp

-g embeds source-level information (file names, line numbers, variable names) into the executable so the debugger can show you your code rather than raw assembly. -O0 keeps the program close to the source so single-stepping makes sense.

A typical gdb session looks like:

$ gdb ./program
(gdb) break main          # pause when main starts
(gdb) run                 # start the program
(gdb) next                # run the current line, then pause
(gdb) step                # step into a function call
(gdb) print x             # show the value of variable x
(gdb) backtrace           # show the call stack
(gdb) continue            # run until the next breakpoint or end
(gdb) quit

The same commands in lldb are very similar (b main, run, n, s, p x, bt, c, q).

Two patterns that pay back the time spent learning them:

  • Reproduce, then break. Set a breakpoint just before the line you suspect, run the program, then use next/step and print to walk the failure live. This is faster than scattering print statements and recompiling.
  • Catch the crash. Just running the program under the debugger and waiting for it to crash is enough — when it segfaults the debugger pauses, and backtrace tells you exactly which line and which call chain got you there.

Tip: Most IDEs (VS Code, CLion, Visual Studio, Qt Creator) drive gdb or lldb under a graphical interface. The buttons map directly to the commands above — “step over” is next, “step into” is step, the variables panel runs print for you. Learn the command-line basics first; the GUI is just a thin layer on top.

15.8 Random Numbers

Generating random numbers comes up surprisingly often — games, simulations, testing, shuffling data. C++ provides a proper random number library in <random>.

15.8.1 The Old Way: rand()

C provides rand() and srand() in <cstdlib>. Their signatures are:

int rand();                       // returns a pseudo-random integer
void srand(unsigned int seed);    // seeds the random number generator
// returns current calendar time (from <ctime>)
time_t time(time_t* arg);

You might see them in older code:

#include <cstdlib>
#include <ctime>
#include <iostream>

int main() {
    srand(static_cast<unsigned>(time(nullptr))); // Seed: current time
    std::cout << rand() % 10 << std::endl;       // 0-9, but biased!

    return 0;
}

This works but has problems. rand() produces low-quality random numbers on many systems. Using % to get a range introduces bias — some numbers come up more often than others. And srand(time(nullptr)) means two programs started in the same second get the same sequence.

Trap: Avoid rand() and srand() in new C++ code. They exist for C compatibility but produce poor randomness and make it easy to introduce subtle bias.

15.8.2 The C++ Way: Engines and Distributions

The <random> library separates two concerns: generating raw random bits (the engine) and shaping those bits into the range and distribution you want (the distribution). The key components and their signatures are:

// std::random_device
unsigned int operator()();   // produces a random seed

// std::mt19937
mt19937(unsigned int seed);  // construct with seed

// distributions
uniform_int_distribution(IntType a, IntType b);    // ints in [a, b]
uniform_real_distribution(RealType a, RealType b); // reals in [a, b)
ResultType operator()(Generator& gen);             // generate a value
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());

    std::uniform_int_distribution<int> track(1, 12);
    std::uniform_real_distribution<double> score(0.0, 10.0);

    std::cout << "Random track: " << track(gen) << std::endl;
    std::cout << "Random score: " << score(gen) << std::endl;

    std::cout << "Cinco tracks al azar: ";
    for (int i = 0; i < 5; ++i) {
        std::cout << track(gen) << " ";
    }
    std::cout << std::endl;

    return 0;
}

Possible output:

Random track: 7
Random score: 3.14159
Cinco tracks al azar: 11 3 7 1 9

Here is what each piece does:

  • std::random_device rd provides a seed from your operating system’s entropy source — truly unpredictable.
  • std::mt19937 gen(rd()) creates a Mersenne Twister engine seeded with that random value. This engine produces high-quality pseudo-random numbers.
  • std::uniform_int_distribution<int> track(1, 12) takes the engine’s output and maps it to an integer in [1, 12], with each value equally likely.
  • std::uniform_real_distribution<double> score(0.0, 10.0) does the same for floating-point values in [0.0, 10.0).

Tip: Create the engine once and reuse it. Creating a new std::mt19937 for every random number is wasteful and can produce poor results if seeded with similar values.

Wut: std::random_device is not guaranteed to be truly random on all platforms. On some systems it may fall back to a pseudo-random generator. In practice, on Linux, macOS, and Windows, it reads from the OS entropy pool and is fine for seeding.

15.8.3 Other Distributions

uniform_int_distribution and uniform_real_distribution give every value in the range an equal chance. But sometimes you want values that cluster around a center — this is a normal distribution (also called a Gaussian or bell curve).

std::normal_distribution<RealType>(RealType mean, RealType stddev);

The mean is the center of the bell curve. The stddev (standard deviation) controls how spread out the values are — about 68% of values fall within one standard deviation of the mean, and about 95% within two.

#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());

    std::normal_distribution<double> rating(7.0, 1.5);

    std::cout << "Diez puntuaciones al azar:" << std::endl;
    for (int i = 0; i < 10; ++i) {
        std::cout << rating(gen) << " ";
    }
    std::cout << std::endl;

    return 0;
}

Most values will be close to 7.0, with occasional values farther away. The <random> header provides many other distributions (Bernoulli, Poisson, etc.), but uniform and normal cover most practical needs.

15.9 Try It: Odds and Ends Starter

Here is a program that rolls dice with <random>, times the work with <chrono>, and uses a static_cast to compute an average. Type it in, compile it, and experiment with it.

#include <chrono>
#include <iostream>
#include <random>

int main() {
    // Seed the engine once, then reuse it
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> die(1, 6);

    // Roll two dice five times
    std::cout << "Five rolls of two dice:\n";
    for (int i = 0; i < 5; ++i) {
        int a = die(gen);
        int b = die(gen);
        std::cout << "  " << a << " + " << b
                  << " = " << (a + b) << "\n";
    }

    // Time a million rolls with steady_clock
    auto start = std::chrono::steady_clock::now();

    long long total = 0;
    const int rolls = 1'000'000;
    for (int i = 0; i < rolls; ++i) {
        total += die(gen);
    }

    auto end = std::chrono::steady_clock::now();
    auto elapsed =
        std::chrono::duration_cast<std::chrono::milliseconds>(
            end - start);

    // static_cast so the division is floating-point, not integer
    double average = static_cast<double>(total) / rolls;

    std::cout << "\nAverage of " << rolls << " rolls: "
              << average << "\n";
    std::cout << "That took " << elapsed.count() << " ms\n";

    return 0;
}

Possible output — your numbers will differ because the rolls are random and the timing depends on your machine:

Five rolls of two dice:
  1 + 6 = 7
  2 + 6 = 8
  5 + 6 = 11
  3 + 5 = 8
  3 + 1 = 4

Average of 1000000 rolls: 3.50481
That took 24 ms

The average lands near 3.5 because each face of the die from 1 to 6 is equally likely.

Some things to try:

  • Replace gen(rd()) with a fixed seed like gen(42) and run the program twice. The rolls repeat exactly — handy for reproducing a bug.
  • Cast the elapsed duration to std::chrono::microseconds instead of milliseconds and see how much detail you gain.
  • Remove the static_cast<double> and predict what the average becomes before you run it.
  • Swap the die for a std::normal_distribution<double> with a mean of 3.5 and see how the average and the individual values change.

15.10 Key Points

  • exit() terminates the program from any function; prefer return from main() when possible.
  • EXIT_SUCCESS and EXIT_FAILURE are portable constants for exit codes.
  • C++ mangles function names to support overloading; C does not.
  • extern "C" disables name mangling so C and C++ code can link together.
  • Use #ifdef __cplusplus guards to write headers that work in both C and C++.
  • To the CPU, everything is a number — types tell the compiler how to interpret the bits.
  • A char is just a small integer; 'A' is 65.
  • Different bit widths give different value ranges; overflow wraps around.
  • Prefer static_cast for safe conversions, dynamic_cast for safe downcasting, and avoid const_cast and reinterpret_cast unless necessary.
  • Never use C-style casts in new C++ code — use the named C++ casts instead.
  • Use std::chrono::steady_clock to measure elapsed time; duration_cast converts between time units but truncates.
  • Avoid rand() and srand() — use <random> with an engine (std::mt19937) and a distribution (std::uniform_int_distribution, etc.).
  • Seed the engine with std::random_device for unpredictable results.
  • std::normal_distribution generates values clustered around a mean with a given standard deviation (bell curve).
  • <cmath> provides sqrt, pow, floor, ceil, round, fmod, trig, log, and exp functions; trig functions take radians.
  • <numbers> (C++20) provides typed compile-time math constants like std::numbers::pi — prefer them over hand-typed digits.
  • Undefined behavior means anything can happen; the compiler is allowed to assume your code does not exhibit it. Compile with -Wall -Wextra -fsanitize=address,undefined during development.
  • A debugger (gdb, lldb) lets you pause a program, inspect variables, and walk through it one line at a time. Compile with -g -O0 for debug builds.

15.11 Exercises

  1. What does the following program print if the file data.txt does not exist?

    #include <cstdlib>
    #include <fstream>
    #include <iostream>
    
    void read_file() {
        std::ifstream f("data.txt");
        if (!f) {
            std::cout << "A" << std::endl;
            exit(EXIT_FAILURE);
        }
        std::cout << "B" << std::endl;
    }
    
    int main() {
        read_file();
        std::cout << "C" << std::endl;
        return EXIT_SUCCESS;
    }
  2. What is name mangling, and why does C++ do it but C does not?

  3. A coworker writes the following C++ code to call a C library function but gets a linker error about an undefined symbol. What is the fix?

    // my_program.cpp
    #include <iostream>
    
    void c_library_init();
    
    int main() {
        c_library_init();
        std::cout << "Ready" << std::endl;
        return 0;
    }
  4. What is the value of x after this code runs?

    uint8_t x = 250;
    x = x + 10;
  5. What does the following program print?

    #include <iostream>
    
    int main() {
        char c = 48;
        std::cout << c << std::endl;
        std::cout << static_cast<int>(c) << std::endl;
        return 0;
    }
  6. Explain why this C-style cast is dangerous and what C++ cast you should use instead:

    void* ptr = get_some_pointer();
    int* ip = (int*)ptr;
  7. Both of the following try to convert a double value of 3.14 to an int:

    static_cast<int>(3.14)
    reinterpret_cast<int>(3.14)

    What does each one do? Will the second one even compile?

  8. What does the #ifdef __cplusplus guard accomplish in a C/C++ shared header? When would the code inside the #ifdef be skipped?

  9. Write a program that takes an int and prints it as a char, and takes a char and prints its integer value. Use static_cast for both conversions. Test it with the value 65 and the character 'Z'.

  10. What does the following program print?

    #include <chrono>
    #include <iostream>
    
    int main() {
        using namespace std::chrono;
    
        auto d = seconds(5) + milliseconds(750);
        std::cout << duration_cast<seconds>(d).count() << std::endl;
    
        return 0;
    }
  11. The <chrono> library offers two general-purpose clocks:

    • std::chrono::steady_clock
    • std::chrono::system_clock

    Why should you use the first to measure how long a piece of code takes to run?

  12. What is wrong with this code for generating a random number between 1 and 100?

    #include <cstdlib>
    #include <iostream>
    
    int main() {
        int r = rand() % 100 + 1;
        std::cout << r << std::endl;
    
        return 0;
    }
  13. Write a program that uses <random> to simulate rolling two six-sided dice 10 times and prints each roll.

  14. Write a program that uses std::normal_distribution<double> to generate 10 random values with a mean of 100 and a standard deviation of 15. Print each value. Are most values close to 100?

  15. What does this print?

    #include <iostream>
    
    struct Track { virtual ~Track() = default; };
    struct AudioTrack : Track {
        void play() { std::cout << "audio\n"; }
    };
    struct VideoTrack : Track {
        void play() { std::cout << "video\n"; }
    };
    
    void play(Track *t) {
        if (auto *a = dynamic_cast<AudioTrack *>(t)) {
            a->play();
        } else if (auto *v = dynamic_cast<VideoTrack *>(t)) {
            v->play();
        } else {
            std::cout << "unknown\n";
        }
    }
    
    int main() {
        AudioTrack a;
        VideoTrack v;
        Track      t;
        play(&a);
        play(&v);
        play(&t);
        return 0;
    }

    Why does the base class need a virtual destructor for dynamic_cast to work here?

  16. Where is the bug?

    #include <cctype>
    #include <iostream>
    #include <string>
    
    void uppercase_first(const std::string &s) {
        char &first = const_cast<char &>(s[0]);
        first = static_cast<char>(std::toupper(first));
    }
    
    int main() {
        const std::string title = "wonderwall";
        uppercase_first(title);
        std::cout << title << "\n";
        return 0;
    }

    The function compiles, but it is undefined behavior at runtime. Explain what const_cast is doing here and why this particular use of it is broken.

  17. Calculation: Given the fixed-width types from <cstdint>, how many bytes does each of the following take, and what is the largest value it can hold?

    int8_t   a;
    uint8_t  b;
    int16_t  c;
    uint32_t d;
    int64_t  e;

    Why prefer int32_t over int when you need exactly 32 bits, and why prefer int over int32_t for ordinary counters?

  18. Think about it: What is the difference between calling std::exit(0) and writing return 0; from main? Specifically, which destructors run in each case? Sketch a small program with a class that prints from its destructor and predict what each version prints.

  19. Write a program that seeds a std::mt19937 from std::random_device, then uses it with a std::uniform_int_distribution<int>(1, 100) to print 10 random integers in the range [1, 100]. Now run the program twice and compare: do you get the same numbers each time? Then change the program to use a fixed seed (std::mt19937 rng(42);) and run it twice again. What changed, and why?

  20. Calculation: Use <cmath> and <numbers> to write the area and circumference of a circle with radius r = 4.0. Show the formulas you used and the values you computed. Why is std::numbers::pi preferable to typing 3.14159265 in your code?

  21. What does this print?

    #include <cmath>
    #include <iostream>
    
    int main() {
        std::cout << std::floor(-1.5) << " "
                  << std::ceil(-1.5)  << " "
                  << std::round(-1.5) << " "
                  << std::fmod(7.5, 2.0) << "\n";
        return 0;
    }

    Predict each value before you compile it.

  22. Think about it: A program reads int x; without initializing it and then runs std::cout << x;. The first time you run it, it prints 0. The second time, it prints 32767. The third time, it prints 0 again. Is the program correct? What is the rule that makes this undefined behavior, and what should you do to make the program reliably print 0?

  23. Write a debugger session (in pseudocode — you do not have to actually run it). Given this buggy program:

    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> v;
        for (int i = 0; i < 5; ++i) {
            v.push_back(i);
        }
        for (std::size_t i = 0; i <= v.size(); ++i) {   // bug
            std::cout << v[i] << "\n";
        }
        return 0;
    }

    List the gdb (or lldb) commands you would issue to:

    • set a breakpoint inside the second loop,
    • run the program,
    • inspect i and v.size() at each iteration,
    • identify the iteration on which the program reads past the end of v. What compile flags would you use to build this program for the debugger?