Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Abstract

In this first lecture, we will aim to learn enough to run C++ programs. This will involve understanding the programming toolchain, which is a set of tools used by programmers to write, maintain, and build software.

Initialization

To ensure the notebook runs correctly, execute the following code each time the kernel is started or restarted to initialize the environment:

from __init__ import *

This will import the necessary libraries such as ROOT for running a C++ interpreter.

Motivation

Interactive Jupyter Notebooks

If you accidentally wandered into this territory, run the following cell to see the full name of the course:

%%cpp
cout << string("CS2310") +
     " Computer Programming\n";
CS2310 Computer Programming

%%cpp is a cell magic from a data analysis framework ROOT for running code with a C++ interpreter cling. It is a wonderful tool for learning a “compiled” language C++, but without compilation!

%%cpp
cerr << "500 Internal Error: You ran your code without compiling again! 😡\n";
500 Internal Error: You ran your code without compiling again! 😡

There is nothing wrong. The above code just prints to the standard error stream std::cerr instead of the standard output stream std::cout.

Let’s make the code more interactive:

%%cpp
bool ans;
cout << "You’re taking CS2310 this semester? [0:false, 1:true]\n";
cin >> ans;
cout << (ans 
    ? "200 OK: Welcome aboard, brave coder! 🚀\n" 
    : "302 Found: Redirecting you to a more suitable course... 😅\n");
You’re taking CS2310 this semester? [0:false, 1:true]
302 Found: Redirecting you to a more suitable course... 😅

Did you get redirected? The above program should normally get user input with the standard input stream cin. For this to happen:

  1. Start a terminal with File → New → Terminal.

  2. Run the command

    root
  3. Enter the above program line-by-line or statement-by-statement. (Different from a line, an expression statement is terminated by a semi-colon ;.)

If you really are taking this course, be warned that you are getting yourself into object-oriented programming (OOP). E.g., you will learn to define a class like the following:

%%cpp
#include<string>
#include<iostream>

/**
 * @brief A CityUHK course.
 */
class Course {
private:
    std::string code; ///< The code of the course.
    std::optional<std::string> name; ///< The optional name of the course.

public:
    /**
     * @brief Constructs a new Course.
     *
     * @param code The course code.
     * @param name The optional course name (default to nullopt).
     */
    Course(std::string code, std::optional<std::string> name = std::nullopt)
        : code(std::move(code)), name(std::move(name)) { }

    /**
     * @brief Gets the course code.
     *
     * @return The course code.
     */
    inline const std::string& get_code() const {
        return code;
    }

    /**
     * @brief Sets the course code if the new code is not empty.
     *
     * @param new_code The new code to set.
     */
    inline void set_code(const std::string& new_code) {
        if (!new_code.empty()) { code = new_code; }
    }

    /**
     * @brief Gets the course name.
     *
     * @return The the course name.
     */
    inline const std::optional<std::string>& get_name() const {
        return name;
    }

    /**
     * @brief Sets the course name or removes it if the new name is empty.
     *
     * @param new_name The new course name to set.
     */
    inline void set_name(const std::string& new_name) {
        if (!new_name.empty()) {
            name = new_name;
        } else {
            name = std::nullopt;
        }
    }

    /**
     * @brief Gets the course full name (code bollowed by name).
     *
     * @return The course full name.
     */
    std::string get_full_name() const {
        return name.has_value() ? code + " " + name.value() : code;
    }

    /**
     * @brief Prints a welcome message for the course.
     */
    void say_welcome() const {
        std::cout << format("Welcome to {}!\n", this->get_full_name());
    }
};

😨... The hard work for defining a class pays off when you can create and use its objects easily:

%%cpp
Course cs2310("CS2310");
cs2310.say_welcome();
Welcome to CS2310!

The objects can be modified/mutated. E.g., to set or update the name of the course:

ROOT.cs2310.set_name('Computer Programming')
ROOT.cs2310.say_welcome()
Welcome to CS2310 Computer Programming!

To understand how the program works, use %%cpptutor to step through the code:

%%cpptutor
#include<string>
#include<iostream>

/**
 * @brief A CityUHK course.
 */
class Course {
private:
    std::string code; ///< The code of the course.
    std::optional<std::string> name; ///< The optional name of the course.

public:
    /**
     * @brief Constructs a new Course.
     *
     * @param code The course code.
     * @param name The optional course name (default to nullopt).
     */
    Course(std::string code, std::optional<std::string> name = std::nullopt)
        : code(std::move(code)), name(std::move(name)) { }

    /**
     * @brief Gets the course code.
     *
     * @return The course code.
     */
    inline const std::string& get_code() const {
        return code;
    }

    /**
     * @brief Sets the course code if the new code is not empty.
     *
     * @param new_code The new code to set.
     */
    inline void set_code(const std::string& new_code) {
        if (!new_code.empty()) { code = new_code; }
    }

    /**
     * @brief Gets the course name.
     *
     * @return The the course name.
     */
    inline const std::optional<std::string>& get_name() const {
        return name;
    }

    /**
     * @brief Sets the course name or removes it if the new name is empty.
     *
     * @param new_name The new course name to set.
     */
    inline void set_name(const std::string& new_name) {
        if (!new_name.empty()) {
            name = new_name;
        } else {
            name = std::nullopt;
        }
    }

    /**
     * @brief Gets the course full name (code bollowed by name).
     *
     * @return The course full name.
     */
    std::string get_full_name() const {
        return name.has_value() ? code + " " + name.value() : code;
    }

    /**
     * @brief Prints a welcome message for the course.
     */
    void say_welcome() const {
        std::cout << format("Welcome to {}!\n", this->get_full_name());
    }
};

int main() {
    Course cs2310("CS2310");
    cs2310.say_welcome();
    return 0;
}
Loading...

The same program can also be shared and run in OPT_CPP, which compiles and runs C++ in the browser, so nothing needs to be installed. The following permalink reproduces exactly the program in the cell above:

Open the program in OPT_CPP

The permalink is generated by the Permalink button in OPT_CPP’s Live Mode, which encodes the entire state of the program as query parameters in the URL fragment (the part after #).

For details, you can lookup a C++ reference manual cppmanlite, which is A lightweight, serverless C++ documentation lookup tool written in pure Python to search for any standard library function and get clean documentation instantly.

Agentic Coding

If you need immediate help, execute the %%hermes to send the cell content to a self-learning AI Agent called Hermes powered by a large language model (LLM).

%%hermes
Define a class `Course` in C++ for constructing different course objects by 
providing their course code and, optionally, name.
The object should have:
- a method `say_welcome()` that prints "Welcome to {code} {name}!", if the 
  the course name is provided, or "Welcome to {code}!" otherwise.
- a `set_name` method that modifies the course name if provided a non-empty 
  string as an argument, or remove the course name if an empty string is given.
Loading...
🌿 Fork/new: main (will create on click)

You can also use a chat interface to chat with the Hermes persona. Try introducing yourself in a chat. Hermes can remember you.

Let’s rename the course:

@interact(name="Computer Programming Design")
def update_cs2310(name):
    ROOT.cs2310.set_name(name)
    print(ROOT.cs2310.get_full_name())
Loading...

You can interact with the program by modifying the text box above. (Grab your chance to change the course name to whatever you like!)

CILOs

Now we come to the important question:

What is the course intended learning outcomes (CILOs)?

You can read about them on the frontpage of the book, which is accessible without login:

To be more concrete, we hope that after completing the course, you will have honed the computational thinking skills needed to create a fantastic project like one of the following open-source projects:

Building C++ Projects with AI Agents

Two open-source C++ projects developed with the help of AI agents exemplify the intended learning outcome of this course — building C++ projects with agentic coding techniques:

  • jupyter-cpp-tutor: A Jupyter extension that provides the %%cpptutor cell magic used earlier in this notebook. It compiles C++ code with g++ -g, traces the execution with GNU Debugger (GDB), and renders a step-by-step visualization of the variables, the call stack, and the pointer relationships right inside a notebook cell.

  • OPT_CPP: A serverless C++ visualizer that runs in the browser. It compiles and runs C++ using WebAssembly via xeus-cpp, so nothing needs to be installed. It also includes an Ask AI feature powered by WebLLM that runs an LLM in the browser to help identify bugs.

Both projects were motivated by the challenge of sharing and debugging C++ programs, faced by students of this course. They can be built within a short period of time using locally deployed Agentic Coding tools. The AI agents did most of the building in a inner agent loop, while we test the build and report errors to AI to fix in an outer human-agent loop. In this course, you will get to use the AI tools to build your own project!

History of C++

C++ is a language with a long and rich history. Before we dive into its toolchain, you may want to take a look at this documentary about the people and events that shaped the language:

Compilation

A C++ program is typically compiled into machine code before it can be executed directly by the CPU/GPU/NPU. The compilation process is complex because it must target different platforms (such as various architectures and operating systems) and serve different purposes (such as release versus debugging builds).

How to run a C++ program?

To illustrate this process, run the following cells to create the source code of a hello-world program:[4]

!mkdir -p private  # Create a private directory for files to be generated
%%writefile 'private/hello.cpp'
#include <iostream>
#include <format>

int main() {
    auto who="World";
    std::cout << std::format("Hello, {}!\n", who);
    return 0;
}
Overwriting private/hello.cpp

The C++ source code file is called hello.cpp.[5] The following shell command uses a C++ compiler to compile the source code to an executable (file).

%%bash
# change directory
cd private && \
# compile the source
clang++ \
    -fuse-ld=lld \
    --std=c++23 \
    --stdlib=libc++ \
    hello.cpp \
    -o hello && \
# list the files
ls -la hello.cpp hello
-rwxr-xr-x 1 jovyan users 322376 Sep  3 20:12 hello
-rw-r--r-- 1 jovyan users    141 Sep  3 20:12 hello.cpp

From the output, note that:

  1. The size of the executable hello is significantly larger than that of the source code hello.cpp.

  2. The file permission string -rwxr-xr-x indicates that the file is executable by the user, group, and others, as denoted by the x characters in the three different locations.

To run the executable:

%%bash
./private/hello
Hello, World!

To see the returned value, run the following cell right after executing the hello-world program.

%%bash
echo ${?}
0

How does the original source code translate into an executable?

The compilation can be broken down into several steps.

Step 1: Preprocessor

The first step expands the source code to include the content from the required header files:

%%bash
cd private && \
clang++ --std=c++23 --stdlib=libc++ -E hello.cpp -o hello.ii && \
ls -la hello.cpp hello.ii
-rw-r--r-- 1 jovyan users     141 Sep  3 20:12 hello.cpp
-rw-r--r-- 1 jovyan users 2456207 Sep  3 20:21 hello.ii
Solution to Exercise 1

The function is located at the end of the file. The preprocessor directive is substituted by the content of the header file /usr/lib/llvm-22/bin/../include/c++/v1/iostream.

Step 2: Compiler

Then, the expanded source code is converted to an assembly code in the so-called code generation phase.

%%bash
cd private && \
clang++ --std=c++23 -S hello.ii -o hello.s && \
ls -la hello.ii hello.s
-rw-r--r-- 1 jovyan users 2456207 Sep  3 20:21 hello.ii
-rw-r--r-- 1 jovyan users 2817038 Sep  3 20:25 hello.s

Note that the compiled code is much smaller than the original source code. We can use the optimization flag -O to instruct the compiler to apply various optimization techniques to improve the generated code for different purposes:

%%bash
cd private
clang++ --std=c++23 -S -O0 hello.ii -o hello_O0.s   # no optimization (for debugging)
clang++ --std=c++23 -S -O1 hello.ii -o hello_O1.s   # between -O0 and -O2
clang++ --std=c++23 -S -O2 hello.ii -o hello_O2.s   # moderate level for stability and size in addition to performance
clang++ --std=c++23 -S -O3 hello.ii -o hello_O3.s   # maximum optimization for performance
clang++ --std=c++23 -S -O3 -ffast-math hello.ii -o hello_Of.s  # aggressive, might violate standards
clang++ --std=c++23 -S -Og hello.ii -o hello_Og.s   # optimize while keeping debugging smooth
clang++ --std=c++23 -S -Os hello.ii -o hello_Os.s   # optimize for size
clang++ --std=c++23 -S -Oz hello.ii -o hello_Oz.s   # optimize for more size reduction in the expense of performance
%%bash
ls -la private/hello*.s
-rw-r--r-- 1 jovyan users 2817038 Aug 25 15:03 private/hello.s
-rw-r--r-- 1 jovyan users 2817038 Aug 25 15:07 private/hello_O0.s
-rw-r--r-- 1 jovyan users  804000 Aug 25 15:07 private/hello_O1.s
-rw-r--r-- 1 jovyan users  826868 Aug 25 15:07 private/hello_O2.s
-rw-r--r-- 1 jovyan users  823529 Aug 25 15:07 private/hello_O3.s
-rw-r--r-- 1 jovyan users  817314 Aug 25 15:07 private/hello_Of.s
-rw-r--r-- 1 jovyan users  931537 Aug 25 15:08 private/hello_Og.s
-rw-r--r-- 1 jovyan users  832152 Aug 25 15:08 private/hello_Os.s
-rw-r--r-- 1 jovyan users 1120786 Aug 25 15:08 private/hello_Oz.s

The two commonly used optimizations are:

  • -O3, which is generally good for software release as it attempts to rearrange the code as much as needed to make the final executable run faster, but such rearrangements make it difficult to debug the source code because the mapping from the machine code to the source is not preserved.

  • -Og, which is good for debugging as it minimizes rearrangements of the source and retains function names and variable scopes, allowing a debugger to step through the execution and show relevant information from the corresponding source code.

Step 3: Asssembler

The assembly code is further converted to a machine code.

%%bash
cd private
clang++ -c hello.s -o hello.o
clang++ -c hello_O0.s -o hello_O0.o
clang++ -c hello_O1.s -o hello_O1.o
clang++ -c hello_O2.s -o hello_O2.o
clang++ -c hello_O3.s -o hello_O3.o
clang++ -c hello_Of.s -o hello_Of.o
clang++ -c hello_Og.s -o hello_Og.o
clang++ -c hello_Os.s -o hello_Os.o
clang++ -c hello_Oz.s -o hello_Oz.o
%%bash
ls -la private/hello*.o
-rw-r--r-- 1 jovyan users 617584 Aug 25 15:08 private/hello.o
-rw-r--r-- 1 jovyan users 617584 Aug 25 15:08 private/hello_O0.o
-rw-r--r-- 1 jovyan users 160728 Aug 25 15:08 private/hello_O1.o
-rw-r--r-- 1 jovyan users 163968 Aug 25 15:08 private/hello_O2.o
-rw-r--r-- 1 jovyan users 161856 Aug 25 15:08 private/hello_O3.o
-rw-r--r-- 1 jovyan users 160648 Aug 25 15:08 private/hello_Of.o
-rw-r--r-- 1 jovyan users 173848 Aug 25 15:08 private/hello_Og.o
-rw-r--r-- 1 jovyan users 171488 Aug 25 15:08 private/hello_Os.o
-rw-r--r-- 1 jovyan users 238528 Aug 25 15:08 private/hello_Oz.o

hello.o is called an object file, which is a binary file containing the binary machine code (opcode+operand) directly understandable by the processor.

Step 4: Linker

The final step is the linking process, which turns one or more object files into an executable.

%%bash
cd private && \
clang++ -fuse-ld=lld --stdlib=libc++ hello.o -o hello_again
Solution to Exercise 3

Omitting --std=c++23 when expanding the source code does not fail immediately. However, subsequent conversion to assembly, whether the option --std=c++23 is specified or not, fails.

error: no member named 'format' in namespace 'std'
    6 |     std::cout << std::format("Hello, {}!\n", who);
      |                  ~~~~~^

This is because std:format from <format> require c++23 standard, but the default standard used by the compiler is currently C++17.

Omitting --stdlib=libc++ causes the linking process will fail with the error:

/opt/conda/bin/ld: cannot find -lstdc++: No such file or directory
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

This is because hello.o needs to be linked to the standard library in order to use the cout object.

%%hermes
Why use the option `-fuse-ld=lld`?
Loading...
🌿 Fork/new: main (will create on click)

Building from Source

A C++ project often consists of multiple source code files and libraries that depend on one another in certain way. E.g.,

addmul.cpp defines the main application that uses functions from

  • io.cpp to get user input;

  • add.cpp to carry out addition of two numbers; and

  • mul.cpp to carry out multiplication of three numbers.

mymath.hpp and util.hpp are header files that declares the functions shared by multiple source code files.

To compile the main application, its dependencies must be compiled first. For large projects with complex interdependencies, manual compilation is both challenging and error-prone, which naturally calls for the following tool in the programming toolchain.

How to build a C++ project with CMake?

Let’s clean up all the build files in case you have run the Make command before:

!make clean-all
rm -rf "build" bin/ lib/ .cache/

Next, run the following command to generate files in a build directory:

!make generate && ls -la build/
/usr/bin/cmake -B "build" -S . -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=
-- The CXX compiler identification is Clang 22.1.8
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/clang++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Setting build type to 'Debug' as none was specified.
-- Configuring done (2.6s)
-- Generating done (0.3s)
-- Build files have been written to: /home/jovyan/cs2310i26a/source/Lecture1/build
total 1
drwxr-xr-x  4 jovyan users     8 Sep  3 20:48 .
drwxr-xr-x 13 jovyan users    24 Sep  3 20:48 ..
-rw-r--r--  1 jovyan users 14092 Sep  3 20:48 CMakeCache.txt
drwxr-xr-x  7 jovyan users    15 Sep  3 20:48 CMakeFiles
-rw-r--r--  1 jovyan users  7624 Sep  3 20:48 Makefile
-rw-r--r--  1 jovyan users  2319 Sep  3 20:48 cmake_install.cmake
-rw-r--r--  1 jovyan users  2414 Sep  3 20:48 compile_commands.json
drwxr-xr-x  3 jovyan users     5 Sep  3 20:48 src

The above Make command executes the CMake command

/usr/bin/cmake -B "build" -S . -DCMAKE_CXX_COMPILER=clang++

where

  • -B "build" specifies the build directory where CMake will generate the build system files and perform the build process.

  • -S . specifies the source directory containing the CMake configuration file CMakeLists.txt.

  • -DCMAKE_CXX_COMPILER=/usr/bin/clang++ sets the C++ compiler to /usr/bin/clang++ for compiling C++ source files.

Next, to compile the source code into binary files:

!make build && ls -la bin/ lib/ build/
/usr/bin/cmake --build "build"
gmake[1]: Entering directory '/home/jovyan/cs2310i26a/source/Lecture1/build'
[ 14%] Building CXX object CMakeFiles/mymath.dir/mymath/add.cpp.o
[ 28%] Building CXX object CMakeFiles/mymath.dir/mymath/mul.cpp.o
[ 42%] Linking CXX shared library /home/jovyan/cs2310i26a/source/Lecture1/lib/libmymath.so
[ 42%] Built target mymath
[ 57%] Building CXX object CMakeFiles/util.dir/util/io.cpp.o
[ 71%] Linking CXX static library libutil.a
[ 71%] Built target util
[ 85%] Building CXX object src/CMakeFiles/addmul.dir/addmul.cpp.o
[100%] Linking CXX executable /home/jovyan/cs2310i26a/source/Lecture1/bin/addmul
[100%] Built target addmul
gmake[1]: Leaving directory '/home/jovyan/cs2310i26a/source/Lecture1/build'
bin/:
total 1
drwxr-xr-x  2 jovyan users      3 Sep  3 20:49 .
drwxr-xr-x 13 jovyan users     24 Sep  3 20:49 ..
-rwxr-xr-x  1 jovyan users 681144 Sep  3 20:49 addmul

build/:
total 14
drwxr-xr-x  4 jovyan users      9 Sep  3 20:49 .
drwxr-xr-x 13 jovyan users     24 Sep  3 20:49 ..
-rw-r--r--  1 jovyan users  14092 Sep  3 20:48 CMakeCache.txt
drwxr-xr-x  7 jovyan users     15 Sep  3 20:49 CMakeFiles
-rw-r--r--  1 jovyan users   7624 Sep  3 20:48 Makefile
-rw-r--r--  1 jovyan users   2319 Sep  3 20:48 cmake_install.cmake
-rw-r--r--  1 jovyan users   2414 Sep  3 20:48 compile_commands.json
-rw-r--r--  1 jovyan users 120908 Sep  3 20:49 libutil.a
drwxr-xr-x  3 jovyan users      5 Sep  3 20:48 src

lib/:
total 5
drwxr-xr-x  2 jovyan users     3 Sep  3 20:48 .
drwxr-xr-x 13 jovyan users    24 Sep  3 20:49 ..
-rwxr-xr-x  1 jovyan users 17320 Sep  3 20:48 libmymath.so

The CMake command option --build "build" instructs CMake to look for the build system files in the build subfolder and compile the source code to generate:

  • lib/libmymath.so, a dynamic/shared library providing the add and mul functions;

  • build/libutil.a, a static library providing the get_input function; and

  • bin/addmul, the main application.

%%bash
make run <<EOF
1
2
3
EOF
"./bin/addmul"
Enter an integer: Enter an integer: Enter an integer: 
add(1, 2) == 3
mul(1, 2, 3) = 6

Try opening a terminal to run the executable "${HOME}/cs2310_26a/Lecture1/bin/addmul" directly.

How to edit and debug the source code?

Although Jupyter does not have visual debugging capability for C++ code, we can use the Visual Student Code (VS Code) interface.

Before starting VS Code, run the following cell for a one-time installation of some useful VSCode extensions for debugging:

%%bash
if [ -z "${NBGRADER_EXECUTION}" ]; then
  make setup
fi
Setting up clangd for Code Server...
Installing extensions...
Extension 'llvm-vs-code-extensions.vscode-clangd' v0.6.0 is already installed. Use '--force' option to update to latest version or provide '@<version>' to install a specific version, for example: 'llvm-vs-code-extensions.vscode-clangd@1.2.3'.
Installing extensions...
(node:30402) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
(Use `node --trace-deprecation ...` to show where the warning was created)
Installing extension 'vadimcn.vscode-lldb'...
Extension 'vadimcn.vscode-lldb' v1.12.3 was successfully installed.
Done!

Run the cell below to generate the link to open Lecture1 in VS Code:

vscode('.', target='VSC1')
Loading...

Alternatively,

  1. launch VS Code from JupyterLab with File → New Launcher and then

  2. open Lecture1 folder in VS Code with ☰ → File → Open Folder....

Prior to debugging, you should have built the project and configured the debugger for the project with a file .vscode/launch.json at the project root folder:

!mkdir -p .vscode
%%writefile .vscode/launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://code.visualstudio.com/docs/debugtest/debugging#_launch-configurations
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch",
            "type": "lldb",
            "request": "launch",
            "program": "${workspaceFolder}/bin/addmul",
            "args": [],
            "stopOnEntry": false
        }
    ]
}
Writing .vscode/launch.json

As commented in the first line, you can learn more about the configuration using IntelliSense in VS Code. We enabled IntelliSense for C++ earlier by the following steps:

  • Install a C++ language server extension: Install an extension such as clangd. This was done with make setup.

  • Export the compilation commands: Create the compile_commands.json file in the build directory. This was done with make generate.

Solution to Exercise 4
  1. This is done by set(CMAKE_EXPORT_COMPILE_COMMANDS ON).

  2. This is done by set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE)

To keep the intermediate files, uncomment the line # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -save-temps").

Follow the steps below to debug.

  1. Open source files:

    • Navigate to the Explorer view by clicking on the folder icon on the sidebar or pressing Ctrl/Cmd+Shift+E.

    • Find and open the addmul.cpp file.

  1. Set a breakpoint:

    • Click on the line number next to int main() { to set a breakpoint. A red dot will appear indicating the breakpoint.

    • Navigate to the Run and Debug view by clicking on the play-with-a-bug icon on the sidebar or pressing Ctrl/Cmd+Shift+D

    • Click on the green play button (or press F5) to start debugging. This will launch the program and pause execution at the breakpoint.

  1. Inspect variables:

    • Use the Variables pane to view the current values of variables like a, b, and c.

    • Observe how these values change as you step through the code.

  1. Examine the call stack:

    • The Call Stack pane shows the sequence of function calls leading to the current point in the program.

    • This helps understand the flow of execution and how values are passed between functions.

  1. Watch expressions:

    • Add expressions to the Watch pane (usually on the left side during debugging) to monitor specific calculations or function calls. E.g., watch the expression \nat mul(a, b, c). See the manual for codelldb.

  1. Step through the code:

    • Step Over: Press F10 to execute the next line of code without stepping into any function calls. E.g.,

      • try step over until you reach the call to mul(a, b, c);

      • enter some non-zero integers in the terminal when prompted; and

      • observe the Variables and Watch panes.

    • Step Into: Press F11 to step into the function call on the current line. This allows you to inspect the function’s internal operations. E.g.,

      • try stepping into the call of mul(a, b, c); and

      • stepping out of it.

    • Continue: Press F5 to continue running the program until the next breakpoint or the end of the program.

To rebuild the project for release instead of debugging:

  1. Access the Terminal by going to the menu and select Terminal>New Terminal or press Ctrl+` (backtick).

  2. Rebuild the project by running make -e CMAKE_BUILD_TYPE=release.

  3. Enter any integer when prompted by the application.

Try running the debugger again to see that it does not stop at any breakpoints. You can set the CMAKE_BUILD_TYPE=release explicitly at the beginning of the Makefile.

Makefile
# Makefile for a C++ project

# =====================================================================
# For building a C++ project with CMake
# ---------------------------------------------------------------------

# Directory where the build artifacts will be stored
BUILD_DIR ?= build

# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
# Options: "Debug" "Release" "MinSizeRel" "RelWithDebInfo"
# Default is set to "Debug" in ./CMakelists.txt
CMAKE_BUILD_TYPE ?=

# Set the C++ compiler
# Default to $CXX 
CMAKE_CXX_COMPILER ?= clang++

export CXX CMAKE_BUILD_TYPE

# Use bash for Make commands
SHELL:=/bin/bash

# Path to cmake executable
CMAKE_COMMAND ?= $(shell which cmake)

# Directory where `make install` places the artifacts.
# Defaults to the user-local prefix ~/.local (no sudo required).
# Override with, e.g., make install PREFIX=/home/me/.local
PREFIX ?= $(HOME)/.local

# Phony targets that are not actual files, but rather a command to be executed
.PHONY: all generate build run clean install uninstall clean-all vscode-clangd

# The main target to build the project
all: generate build run

# Generate compile commands for the project
# See:
# - CMake manual: https://cmake.org/cmake/help/latest/manual/cmake.1.html#generate-a-project-buildsystem
generate:
	$(CMAKE_COMMAND) -B "$(BUILD_DIR)" -S . -DCMAKE_CXX_COMPILER=$(CMAKE_CXX_COMPILER) -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) -DCMAKE_INSTALL_PREFIX=$(PREFIX)

# Build the project after generate
# See: https://cmake.org/cmake/help/latest/manual/cmake.1.html#build-a-project
build:
	$(CMAKE_COMMAND) --build "$(BUILD_DIR)"

# Run the executable after build
run:
	"./bin/addmul"

# Install the artifacts (executable, libraries, headers) under $(PREFIX)
# See: https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project
# The executable finds the shared library relative to itself ($ORIGIN/../lib),
# so no LD_LIBRARY_PATH is needed; only $(PREFIX)/bin may need a PATH entry.
# `generate` is re-run so that overriding PREFIX (e.g.
# `make install PREFIX=/home/me/.local`) always takes effect — CMake caches
# the prefix from the previous configure otherwise.
install: generate build
	$(CMAKE_COMMAND) --install "$(BUILD_DIR)"
	@echo "Installed to $(PREFIX)."
	@echo "If $(PREFIX)/bin is not on your PATH, add the following to ~/.bashrc:"
	@echo "  export PATH=\"$(PREFIX)/bin:\$$PATH\""

# Remove the files previously installed by `make install`
# (the uninstall script is generated at configure time by CMakeLists.txt)
uninstall:
	$(CMAKE_COMMAND) -P "$(BUILD_DIR)/cmake_uninstall.cmake"

# Clean up the artifacts
clean:
	$(CMAKE_COMMAND) --build "$(BUILD_DIR)" --target clean

# Clean up the build system
clean-all:
	rm -rf "$(BUILD_DIR)" bin/ lib/ .cache/


# =====================================================================
# For one-time setup
# ---------------------------------------------------------------------

# Install and configure clangd a debugger extension for Coder Server
# See:
# - CodeLLDB manual: https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#expressions
# - VSCode clangd: https://github.com/clangd/vscode-clangd
# - VSCode debug: https://code.visualstudio.com/docs/debugtest/debugging
setup:
	@echo "Setting up clangd for Code Server..." && \
	code-server --install-extension llvm-vs-code-extensions.vscode-clangd && \
	code-server --install-extension vadimcn.vscode-lldb && \
	echo "Done!"

Different Compilers

GNU Compiler Collection

There are C++ compilers other than clang++. For instance, a popular and mature C++ compiler for Linux is g++ from the GNU Compiler Collection (GCC):

%%bash
cd private && \
g++ --std=c++23 hello.cpp -o hello_gcc && \
./hello_gcc
Hello, World!

The executables compiled from the same source code can be different for different compilers, as they use different compiler configurations and libraries:

%%bash
ls -la ./private/hello_gcc ./private/hello
-rwxr-xr-x 1 jovyan users 322376 Aug 25 15:00 ./private/hello
-rwxr-xr-x 1 jovyan users 250920 Aug 25 15:53 ./private/hello_gcc

Actually Portable Executable

Both clang++ and g++ are installable on nearly any platforms, but the executable compiled in one platform cannot run in another:

%%hermes
Why an executable compiled by GCC in one platform may not run in another platform?
Loading...
↻ Session: main (ACP: 691fa645-f3b…)

However, an executable compiled using the Cosmopolitan Libc framework is an exception. Since Cosmo does not support c++23 standard yet, we will demonstrate using the following source code without using <format>:

%%writefile 'private/hello_cosmo.cpp'
#include <iostream>

int main() {
    std::cout << "Hello, World!\n";
    return 0;
}
Writing private/hello_cosmo.cpp

The cosmoc++ compiler can create a single binary, called Actually Portable Executable (APE), that can run on multiple platforms, including Linux, Windows, and macOS.

%%bash
cd private && \
cosmoc++ hello_cosmo.cpp -o hello_cosmo && \
./hello_cosmo
Hello, World!

As seeing is believing, try running the executable on a platform other than the current platform by following the official documentation.

Emscripten

It is even possible to compile C++ code to a web page that can run in-browser.[6]

%%bash
mkdir -p ~/www/hello && \
em++ --std=c++23 ./private/hello.cpp -o ~/www/hello/index.html && \
ls -la ~/www/hello/*
shared:INFO: (Emscripten: Running sanity checks)
-rw-r--r-- 1 jovyan users  21960 Aug 25 15:56 /home/jovyan/www/hello/index.html
-rw-r--r-- 1 jovyan users 177925 Aug 25 15:56 /home/jovyan/www/hello/index.js
-rwxr-xr-x 1 jovyan users 458280 Aug 25 15:56 /home/jovyan/www/hello/index.wasm

After running the above shell, the web page should be available at:[7]

  • The C++ code is compiled to WebAssembly index.wasm, along with

  • the HTML index.html and JavaScript index.js to serve the WebAssembly.

The JavaScript script file index.js can also be executed using Node.js:[8]

%%bash
node ~/www/hello/index.js
Hello, World!

Supplementary Notes

file permission string

In operating systems, the file permission string represents the read, write, and execute permissions of a file for the owner, group, and others.

%%hermes
Explain the permission string -rwxr-xr-x.

shell command

The %%bash magic runs the cell content using a (bash) shell, which is a command-line interpreter for accessing the operating system’s services.

%%hermes
What is bash and why is it called a "shell"?
%%hermes
What is the standard way to specify command-line options?

exit status code

%%hermes
Briefly list the exit codes for C++ programs.
Loading...
↻ Session: main (ACP: 691fa645-f3b…)
%%hermes
Is running `./private/hello` the same as running `cd private && ./hello` in bash?
Loading...
↻ Session: main (ACP: 691fa645-f3b…)
%%hermes
Is running `./hello` the same as running `hello` in bash?
Loading...
↻ Session: main (ACP: 691fa645-f3b…)

compiler configurations

The following commands use the flags

  • -v to output the details of the compiler’s configuration, and

  • -fsyntax-only to instruct the compiler to only check the syntax of the source code without generating any object code or executable.

%%bash
g++ --std=c++23 -v -fsyntax-only ./private/hello.cpp
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/15/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 15.2.0-16ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-15/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2,rust,cobol,algol68 --prefix=/usr --with-gcc-major-version-only --program-suffix=-15 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-15-j35TAX/gcc-15-15.2.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-15-j35TAX/gcc-15-15.2.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 15.2.0 (Ubuntu 15.2.0-16ubuntu1) 
COLLECT_GCC_OPTIONS='-std=c++23' '-v' '-fsyntax-only' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'a-'
 /usr/libexec/gcc/x86_64-linux-gnu/15/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE ./private/hello.cpp -quiet -dumpdir a- -dumpbase hello.cpp -dumpbase-ext .cpp -mtune=generic -march=x86-64 -std=c++23 -version -fsyntax-only -o /dev/null -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -fzero-init-padding-bits=all -Wbidi-chars=any
GNU C++23 (Ubuntu 15.2.0-16ubuntu1) version 15.2.0 (x86_64-linux-gnu)
	compiled by GNU C version 15.2.0, GMP version 6.3.0, MPFR version 4.2.2, MPC version 1.3.1, isl version isl-0.27-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/15"
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/15/include-fixed/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/15/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/15
 /usr/include/x86_64-linux-gnu/c++/15
 /usr/include/c++/15/backward
 /usr/lib/gcc/x86_64-linux-gnu/15/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
Compiler executable checksum: b661a74662ad7cf413c4193391ab5290
COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/15/:/usr/libexec/gcc/x86_64-linux-gnu/15/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/15/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/15/:/usr/lib/gcc/x86_64-linux-gnu/15/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/15/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/15/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-std=c++23' '-v' '-fsyntax-only' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'a.'
%%bash
clang++ --std=c++23 -v -fsyntax-only --stdlib=libc++ ./private/hello.cpp
Ubuntu clang version 22.1.8 (++20260714015917+ca7933e47d3a-1~exp1~20260714135927.17)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-22/bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/14
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/15
Candidate multilib: .;@m64
Selected multilib: .;@m64
 (in-process)
 "/usr/lib/llvm-22/bin/clang" -cc1 -triple x86_64-pc-linux-gnu -fsyntax-only -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name hello.cpp -mrelocation-model pic -pic-level 2 -pic-is-pie -mframe-pointer=all -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/home/jovyan/cs2310i26a/source/Lecture1 -v -fcoverage-compilation-dir=/home/jovyan/cs2310i26a/source/Lecture1 -resource-dir /usr/lib/llvm-22/lib/clang/22 -internal-isystem /usr/lib/llvm-22/bin/../include/c++/v1 -internal-isystem /usr/lib/llvm-22/lib/clang/22/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include --std=c++23 -fdeprecated-macro -ferror-limit 19 -fgnuc-version=4.2.1 -fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -faddrsig -fdwarf2-cfi-asm -x c++ ./private/hello.cpp
clang -cc1 version 22.1.8 based upon LLVM 22.1.8 default target x86_64-pc-linux-gnu
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/15/../../../../x86_64-linux-gnu/include"
ignoring nonexistent directory "/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/llvm-22/bin/../include/c++/v1
 /usr/lib/llvm-22/lib/clang/22/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
%%bash
em++ --std=c++23 -v -fsyntax-only --stdlib=libc++ ./private/hello.cpp
 "/opt/emsdk/upstream/bin/clang++" -target wasm32-unknown-emscripten -fignore-exceptions -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr --sysroot=/opt/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat --std=c++23 -v -fsyntax-only --stdlib=libc++ ./private/hello.cpp
clang version 24.0.0git (https:/github.com/llvm/llvm-project 4bfd08c2d769736841ae4f5705d76fa6daa39027)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /opt/emsdk/upstream/bin
 (in-process)
 "/opt/emsdk/upstream/bin/clang-24" -cc1 -triple wasm32-unknown-emscripten -fsyntax-only -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name hello.cpp -mrelocation-model static -mframe-pointer=none -ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-cpu generic -fvisibility=hidden -debugger-tuning=gdb -fdebug-compilation-dir=/home/jovyan/cs2310i26a/source/Lecture1 -v -fcoverage-compilation-dir=/home/jovyan/cs2310i26a/source/Lecture1 -resource-dir /opt/emsdk/upstream/lib/clang/24 -isysroot /opt/emsdk/upstream/emscripten/cache/sysroot -internal-isystem /opt/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/noeh/c++/v1 -internal-isystem /opt/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/c++/v1 -internal-isystem /opt/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1 -internal-isystem /opt/emsdk/upstream/lib/clang/24/include -internal-isystem /opt/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten -internal-isystem /opt/emsdk/upstream/emscripten/cache/sysroot/include --std=c++23 -fdeprecated-macro -ferror-limit 19 -fgnuc-version=4.2.1 -fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fignore-exceptions -fexceptions -iwithsysroot/include/fakesdl -iwithsysroot/include/compat -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -x c++ ./private/hello.cpp
clang -cc1 version 24.0.0git based upon LLVM 24.0.0git default target x86_64-unknown-linux-gnu
ignoring nonexistent directory "/opt/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/noeh/c++/v1"
ignoring nonexistent directory "/opt/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/c++/v1"
ignoring nonexistent directory "/opt/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten"
#include "..." search starts here:
#include <...> search starts here:
 /opt/emsdk/upstream/emscripten/cache/sysroot/include/fakesdl
 /opt/emsdk/upstream/emscripten/cache/sysroot/include/compat
 /opt/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1
 /opt/emsdk/upstream/lib/clang/24/include
 /opt/emsdk/upstream/emscripten/cache/sysroot/include
End of search list.

header files

A header file in C++ is a file with a .h or .hpp extension that contains declarations and definitions that can be shared across multiple source files.

%%hermes
Why does C++ use header files? Are header files common in other programming languages?
Loading...
↻ Session: main (ACP: 691fa645-f3b…)

binary file

Hex editors such as radare2 can help us read and edit machine code.

%%hermes
Give a brief but concrete example how one can reverse engineer a software using a hex editor.
Loading...
↻ Session: main (ACP: 691fa645-f3b…)

dynamic library

A dynamic library in C++ is a collection of functions and data that can be loaded and linked at runtime, allowing multiple programs to share the same code. The lib folder is commonly used to store these dynamic library files, such as .so files on Unix-like systems or .dll files on Windows.

For instance, the standard library for clang++ is:

!ldconfig -p | grep libc++.so
	libc++.so.1 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libc++.so.1

The standard library for g++ is:

!ldconfig -p | grep libstdc++
	libstdc++.so.6 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libstdc++.so.6
	libstdc++.so.6 (libc6) => /usr/lib32/libstdc++.so.6

static library

A static library in C++ is a collection of object files combined into a single archive file (typically with a .a extension), which is included directly into the executable at compile time.

%%hermes
When is it preferrable to use a static library instead of a dynamic library, or 
the object files directly.
Loading...
↻ Session: main (ACP: 691fa645-f3b…)
Footnotes
  1. See the Jupyter blog for more details.

  2. This is a sample response generated by the above prompt. Click me to expand the content.

  3. See Compilation for more details.

  4. %%writefile is an IPython cell magic for creating a file with the content of the magic cell.

  5. Other common extensions are: .cc, .cxx, .C and also .cu for CUDA.

  6. Running in-browser means an application can be served statically via a web server and executed directly within a web browser without requiring a sustained internet connection once loaded. E.g., try the WebLLM Chat or its android app without internet after loading a model.

  7. The static page is served via a basic proxy jupyter-www-proxy.

  8. Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine, allowing developers to run JavaScript outside of the browser; for more information, see the official Node.js documentation.