45.1 C++ for Python programmers⧉
The problem sets in this book are written in C++, and if Python is your home language this appendix is the bridge. You are not expected to arrive knowing C++: the early psets ramp you up, and what follows is the map of what changes.
Why C++ at all? It is compiled and close to the metal, so the per-pixel loops that image processing lives on can run far faster than equivalent scalar Python loops; the gain depends on the workload and compiler, and is not a comparison with optimized NumPy kernels. It is still the standard where performance matters, in graphics and imaging, and it is the language Chapter 29.4, Halide programming plugs into for the last chapters. The price is bluntness: more typing, fewer guardrails, and memory you sometimes manage yourself. The good news is that for this book you stay in a small, safe corner of the language.
45.1.1 What changes, coming from Python⧉
- Compiled, not interpreted. You translate the whole program to machine code once, with the compiler, then run the resulting binary. A whole class of mistakes (a misspelled name, a type mismatch) is caught at compile time rather than when the line happens to run.
- Static types, declared up front. Every variable has a fixed type, written before its name:
int n = 5;,float x = 0.5f;. You cannot later rebindnto a string. Functions spell out the type of each argument and of the return value. - Semicolons and braces. Statements end in
;, and blocks are delimited by curly braces{ }, not by indentation. Indent for readability; the compiler ignores it. - Headers and implementation files in the course code. A header (
.h) declares what exists; an implementation (.cpp) defines it. You#includethe header, you do notimport. - Value semantics by default. Assigning or passing an object by value generally copies it, though moves and copy elision can avoid copies, unlike Python, where a name is a reference to a shared object. You ask for sharing explicitly, with a reference.
- A leaner toolkit. Fewer built-in higher-order conveniences; you write more loops by hand. These exercises do not use a C++ array library to do the work for you, so the loop over pixels is yours to write (and the compiler's to make fast).
- Memory is explicit, but mostly hidden here. There is no garbage collector, yet if you stick to local variables and
std::vectoryou almost never callnewordelete.
45.1.2 A first taste, side by side⧉
Brighten an image by a constant factor, written as a function in each language. In Python the body is a single NumPy broadcast:
def brighten(im, factor): return im * factor
Assume floating-point NumPy inputs, with import numpy as np. In C++ with the course's Image class it is an explicit loop, and every difference above shows up at once:
Image brighten(const Image &im, float factor) { Image out(im.width(), im.height(), im.channels()); for (int c = 0; c < im.channels(); c++) for (int y = 0; y < im.height(); y++) for (int x = 0; x < im.width(); x++) out(x, y, c) = im(x, y, c) * factor; return out; }
Types on everything, the loop written out, const Image &im to pass the input by reference without copying it or letting the function change it, and semicolons throughout. The vectorization is gone, but the compiled loop is fast.
45.1.3 A few more comparisons⧉
The same algorithm three ways (the C++ clamp example needs C++17 and <algorithm>): plain Python with the explicit loops C++ makes you write, the C++ itself, and the vectorized NumPy one-liner. The shading marks what C++ adds over the plain-Python loops, only the static types, the punctuation, and std::; the loops themselves are identical. Switch tabs, and copy any of them:
def clamp01(im): out = np.empty_like(im) H, W, C = im.shape for c in range(C): for y in range(H): for x in range(W): out[y, x, c] = min(1.0, max(0.0, im[y, x, c])) return out
Image clamp01(const Image &im) { Image out(im.width(), im.height(), im.channels()); for (int c = 0; c < im.channels(); c++) for (int y = 0; y < im.height(); y++) for (int x = 0; x < im.width(); x++) out(x, y, c) = std::clamp(im(x, y, c), 0.0f, 1.0f); return out; }
def clamp01(im): return np.clip(im, 0.0, 1.0)
def luminance(im): H, W, C = im.shape out = np.empty((H, W, 1)) for y in range(H): for x in range(W): out[y, x, 0] = (0.299 * im[y, x, 0] + 0.587 * im[y, x, 1] + 0.114 * im[y, x, 2]) return out
Image luminance(const Image &im) { Image out(im.width(), im.height(), 1); // one output channel for (int y = 0; y < im.height(); y++) for (int x = 0; x < im.width(); x++) out(x, y, 0) = 0.299f * im(x, y, 0) + 0.587f * im(x, y, 1) + 0.114f * im(x, y, 2); return out; }
def luminance(im): return (0.299 * im[:, :, 0:1] + 0.587 * im[:, :, 1:2] + 0.114 * im[:, :, 2:3])
def blend(a, b, t): out = np.empty_like(a) H, W, C = a.shape for c in range(C): for y in range(H): for x in range(W): out[y, x, c] = (1.0 - t) * a[y, x, c] + t * b[y, x, c] return out
Image blend(const Image &a, const Image &b, float t) { Image out(a.width(), a.height(), a.channels()); for (int c = 0; c < a.channels(); c++) for (int y = 0; y < a.height(); y++) for (int x = 0; x < a.width(); x++) out(x, y, c) = (1.0f - t) * a(x, y, c) + t * b(x, y, c); return out; }
def blend(a, b, t): return (1.0 - t) * a + t * b
def mean(im): H, W, C = im.shape total = 0.0 for c in range(C): for y in range(H): for x in range(W): total += im[y, x, c] return total / (H * W * C)
float mean(const Image &im) { float total = 0.0f; for (int c = 0; c < im.channels(); c++) for (int y = 0; y < im.height(); y++) for (int x = 0; x < im.width(); x++) total += im(x, y, c); return total / (im.width() * im.height() * im.channels()); }
def mean(im): return im.mean()
The luminance example retains the course's grayscale weights and function name: on encoded RGB these weights compute a luma-like quantity, not physical linear-light luminance. Inputs are nonempty; RGB examples need three channels, and blending requires matching shapes.
Across all of them the same shapes recur in the C++: const Image & for every input, im.width()/height()/channels() as the loop bounds, float literals with the f suffix, and a fresh Image out(...) to fill and return (or, for a reduction, a plain float). The plain-Python column makes the real point: the C++ loop is not exotic, it is the loop you would write in Python the moment you step off NumPy's vectorized path, with static types and braces bolted on. Once those are muscle memory the translation is mechanical, and making the loop fast is the compiler's job.
45.1.4 Files and compilation⧉
A C++ project is split in two. The header Image.h declares the class and the function signatures; the implementation Image.cpp defines them. To use another module you #include "Image.h", which literally pastes its declarations in. Because a header can be included along several paths, each one is wrapped in an include guard, an #ifndef / #define / #endif sandwich that stops it being pasted twice; it looks archaic, and the template files already have it. A build tool (make, or CMake) reads a build file listing the sources and runs the compiler, so you compile the whole project with one command.
Two syntax traps trip people early. In a .cpp file the scope operator :: says which class a definition belongs to, as in float Image::operator()(int x, int y, int c), because one file can implement several classes. And a class definition needs a semicolon after its closing brace; forget it and the error surfaces, confusingly, in the next file.
45.1.5 Value, reference, and const⧉
This is the shift that trips Python programmers most, because Python hides it. How you declare a function's parameter decides whether it sees a copy or the original, and whether it may change it.
f(Image im)passes by value: an lvalue argument is copied into the parameter unless the class defines other behavior. Moving a temporary can avoid a copy.f(Image &im)passes by reference: the function works on the original, and changes it makes persist. No copy, so it is fast.f(Image *im)passes a pointer (a raw memory address). We avoid pointers in this book.f(const Image &im)passes by reference but read-only: fast, no copy, and the compiler forbids the function from modifying it. Thisconst &is how you pass most inputs.
A trailing const on a method, int width() const;, is the same promise from the other side: the method will not mutate the object, so getters are marked const and can be called on read-only inputs.
45.1.6 Memory, briefly⧉
An ordinary local object has automatic storage duration and is destroyed when its scope ends; its owned pixel buffer may live in dynamically allocated storage; Image im(w, h, c); is all you need, with no new. Heap allocation with new must be matched by delete, which is where leaks come from, so we lean on std::vector, which owns and frees its own storage. The one sharp edge to remember: array access is unchecked, so v[i] with i out of range does not raise an exception. It silently reads or corrupts unrelated memory, or crashes with a segmentation fault, often far from the actual mistake.
45.1.7 Images in C++⧉
The course wraps an image in a class holding a flat std::vector<float> (often normalized to [0, 1], but intermediate values can lie outside that range) together with its width, height, and channel count. A vector has a single index, so a 3-D image is packed into 1-D by strides. The book, like Halide, stores it plane by plane (all of one channel, then the next):
index of (x, y, c) = c * width * height + y * width + x
An accessor im(x, y, c) hides that arithmetic. It takes x first, unlike NumPy's usual [y, x, c] indexing, and keeps the code consistent with Halide later. The same layout and the reasoning behind it are in Chapter 8.1, Image representation.
45.1.8 Debugging, and the parts that hurt⧉
The recurring frustration of C++ is that errors surface far from their cause: the missing class semicolon breaks the next file, an out-of-bounds write crashes somewhere unrelated, a segmentation fault points at the symptom, not the bug. A few habits from the course make it bearable.
- Read compiler errors from the first one down; later ones are often fallout.
- Test each piece as you write it, on tiny inputs (a 3×3 image, a constant, a single edge) whose answer you can work out by hand. Do not write the whole thing and then run it.
- Print and save intermediates: write images to disk and look at them.
- On a segmentation fault, suspect an array index. Print the index and the size, or
assert(0 <= i && i < size). - Compiler errors and syntax are exactly what search and AI assistants handle well; use them, and acknowledge any AI help per the course policy.
45.1.9 Where to go next⧉
The first problem set (Chapter 45.3, Problem Set 0 — Environment and C++ basics) is a warm-up by design: set up the environment, compile the template, write a few lines with std::vector, and do a brightness or contrast point operation. The psets after it build the Image class, point and neighborhood operations, and onward. For a fuller Python-to-C++ walkthrough, the course points to Goldwasser and Letscher's Python to C++ guide and the cpp4py tutorial.