To begin the journey into modern C++ (C++17 and later), this article walks you through setting up a minimal, professional development environment on macOS, Linux and Windows. Along the way we will write and compile our first program, see how namespaces work, and make it clear why enormous, speed-critical projects like the Google V8 JavaScript engine have to be written in C++.
1. Why is V8 β the brain of Chrome and Node.js β written in C++?
Before installing anything, let's answer the question that is actually worth answering: why do people still reach for C++ for the hardest software, when there are dozens of easier languages available? The clearest example is V8 β the engine that runs JavaScript inside Chrome and Node.js. V8 itself is almost entirely C++.
It comes down to three things, and you will do all three yourself over this series:
- Nothing sits between you and the CPU. When you write JavaScript, another program (V8, as it happens) stands in the middle translating and running it for you. A C++ program is translated straight to machine code before it runs β nobody is in the middle. That is precisely why V8 has to be written in something faster than JavaScript: it is the thing that runs JavaScript.
- You decide when memory is allocated and released. JavaScript has a garbage collector that tidies memory for you β but that garbage collector is itself a piece of C++ someone had to write. Writing it means controlling memory at the lowest level. That is the subject of Lesson 7 (move semantics) and Lesson 8 (smart pointers).
- Abstraction without losing speed (zero-cost abstraction). This is C++'s signature promise: you get to write tidy code with classes, templates and ready-made algorithms β and the compiler turns them into machine code nearly as fast as hand-written assembly. Lessons 10 and 13 show you exactly how.
2. The compilation pipeline & name mangling
Just as in C, C++ source (a .cpp file) has to pass through 4 main stages of the compiler
pipeline before the hardware can run it:
C++ source (.cpp)
β
βΌ [Stage 1: Preprocessor] (handles the #include and #define directives)
Expanded source (.ii)
β
βΌ [Stage 2: Compiler] (translates the source into assembly)
Assembly (.s)
β
βΌ [Stage 3: Assembler] (translates assembly into machine code)
Object code (.o / .obj)
β
βΌ [Stage 4: Linker] (joins the object files and the static/dynamic libraries)
Executable file
Name mangling:
One big difference between a C and a C++ compiler is how they manage the symbol table. C++ supports function overloading β several functions may share a name as long as their parameter types differ. To make that possible, the C++ compiler performs name mangling: it automatically encodes the function name together with its parameter types into a unique symbol inside the object file.
For example, these two functions share a name in C++:
void print(int x); // Compiled to the symbol: _Z5printi
void print(double x); // Compiled to the symbol: _Z5printd
The symbol _Z5printi is not a random string β it can be read: _Z marks an
encoded name, 5 is the length of the original name, print is that name, and
the final letter is the parameter type β i for int, d for
double. You can verify this yourself with the two commands below (on macOS the symbol
carries an extra leading underscore, __Z5printi, because of the Mach-O format; on
Linux/ELF it is exactly as printed above):
# Compile to an object file, then list the symbols inside it
printf 'void print(int x){}\nvoid print(double x){}\n' > nm_demo.cpp
g++ -c nm_demo.cpp -o nm_demo.o
nm nm_demo.o | grep print
# macOS: __Z5printd / __Z5printi (Linux: _Z5printd / _Z5printi)
# Turn a mangled symbol back into a readable signature
echo '_Z5printi' | c++filt
# => print(int)
C, meanwhile, has no name mangling: the function name is kept verbatim as the symbol. So if you want
to write a C++ library that C code can call, you have to use the extern "C" keyword to
tell the C++ compiler not to mangle names in that block:
extern "C" {
void print_c_compatible(int x); // Not mangled - the symbol keeps this exact name
}
3. Installing a modern C++ compiler
On macOS (Clang)
Apple's default compiler is Clang, which ships inside the Command Line Tools. To install it, open Terminal and run:
xcode-select --install
This does not install the full multi-gigabyte Xcode β only the Command Line Tools, which contain
clang++, make and git. A dialog appears; click Install and wait
a few minutes. If your machine already has them, the terminal replies
command line tools are already installed β that is a good result, not an error.
Check the version with clang++ --version. You want a version with solid C++17 support.
On Linux (GCC)
On Debian- or Ubuntu-based systems, install the g++ toolchain with:
sudo apt update
sudo apt install build-essential
build-essential is a bundle: it pulls in g++, gcc,
make and the standard libraries needed to compile anything. You could install each piece
separately, but this is the fastest and most reliable route.
Confirm the version with g++ --version.
On Windows (WSL2 or MSYS2)
Windows ships with no command-line C++ compiler. There are two routes, and for this series I recommend the first:
-
WSL2 (recommended). Install an Ubuntu that runs inside Windows, then use exactly
the Linux commands above β including
aptin section 4. This is how you get every command in the series to behave identically to a Linux machine, with nothing to translate. -
MSYS2. Gives you a native
g++on Windows, producing an.exethat runs without WSL. The trade-off is that libraries are installed withpacmanrather thanapt.
# Option 1 - WSL2: run this in PowerShell as Administrator, then reboot
wsl --install -d Ubuntu
# After the reboot, open "Ubuntu" and follow the Linux instructions above
# Option 2 - MSYS2: install from https://www.msys2.org, then in the MSYS2 UCRT64 shell
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake
g++ --version
Whichever route you pick, the rest of this article still applies: the compile command
g++ -std=c++17 ... is identical on all three operating systems. The only difference is
the name of the resulting file β under MSYS2 on Windows it gains an .exe suffix and is
run as .\hello_cpp.exe.
4. Package managers
As a C++ project grows you will need external libraries (OpenSSL, Boost, CURL and so on). Rather than downloading and building each one by hand, use a package manager to automate it.
On macOS (Homebrew)
Homebrew is the most popular package manager on macOS. If you do not have it yet, install it first:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Then install the common C++ libraries:
# Install Boost (a very widely used utility library)
brew install boost
# Install OpenSSL (cryptography)
brew install openssl
# Install CMake (the build tool)
brew install cmake
On Linux (APT, Debian/Ubuntu)
On Debian-based systems, apt handles packages:
# Refresh the package list
sudo apt update
# Install Boost
sudo apt install libboost-all-dev
# Install OpenSSL and its development headers
sudo apt install libssl-dev
# Install CMake
sudo apt install cmake
5. CMake: a professional build system for C++
Once your project has several source files, external libraries, or has to build on more than one
platform, typing g++ or clang++ by hand stops being workable.
CMake is a cross-platform build tool that lets you describe how the project is built
once, after which CMake generates the Makefiles, Visual Studio projects or Xcode projects
appropriate to the operating system.
The basic CMake project layout
Create a C++ project with this structure:
my_project/
βββ CMakeLists.txt (the CMake configuration file)
βββ src/
β βββ main.cpp
β βββ utils.cpp
βββ include/
β βββ utils.h
βββ build/ (build directory - created when you run CMake)
A sample CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(my_project VERSION 1.0.0 LANGUAGES CXX)
# Set the C++ standard (C++17 or newer)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Per-configuration compiler flags
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -Wextra")
set(CMAKE_CXX_FLAGS_DEBUG "-g -Wall -Wextra")
# Add the include directory (where the .h files live)
include_directories(${CMAKE_SOURCE_DIR}/include)
# List the source files (.cpp)
add_executable(my_program
src/main.cpp
src/utils.cpp
)
# Link against external libraries (Boost, for example)
# target_link_libraries(my_program PRIVATE Boost::boost)
The CMake build workflow
# 1. Create a build directory
mkdir build
cd build
# 2. Run CMake to generate the Makefile
cmake ..
# 3. Compile with make
make
# 4. Run the program
./my_program
# (Optional) Build in Release mode, with optimisation on
cmake -DCMAKE_BUILD_TYPE=Release ..
make
Note that step 2 only runs once: CMake reads CMakeLists.txt and generates a
Makefile inside build/. After each later code change you just type make, and
it recompiles only the file you actually touched. The reason for a separate build/
directory (an out-of-source build) is that every intermediate file stays in one place β delete the
directory and everything is clean, with nothing mixed into your source.
6. Compiler flags β a guide to optimisation & debugging
Here are the compiler flags that matter most for performance and for catching bugs early:
| Compiler flag | What it does | Example |
|---|---|---|
-std=c++17 |
Compile against the C++17 standard | g++ -std=c++17 main.cpp |
-std=c++20 |
Compile against C++20 (concepts, modules) | g++ -std=c++20 main.cpp |
-O0 |
No optimisation (the default; fastest to compile) | g++ -O0 -g main.cpp |
-O1 |
Basic optimisation, compile time still acceptable | g++ -O1 main.cpp |
-O2 |
Balanced optimisation (recommended for production) | g++ -O2 main.cpp |
-O3 |
Maximum optimisation (SIMD vectorisation, aggressive inlining) | g++ -O3 main.cpp |
-Ofast |
Aggressive: abandons strict IEEE floating-point rules | g++ -Ofast main.cpp |
-g |
Emit debug symbols (needed to debug with GDB/LLDB) | g++ -g -O2 main.cpp |
-Wall |
Turn on the basic warnings | g++ -Wall main.cpp |
-Wextra |
Turn on the additional warnings | g++ -Wall -Wextra main.cpp |
-Werror |
Treat warnings as errors (forces you to fix all of them) | g++ -Wall -Werror main.cpp |
-fsanitize=address |
AddressSanitizer: catches buffer overflow and use-after-free | g++ -fsanitize=address main.cpp |
-fPIC |
Position independent code (required for shared libraries) | g++ -fPIC -shared lib.cpp -o lib.so |
-static |
Static linking (every library is embedded in the executable) | g++ -static main.cpp |
Recommended build commands for development vs release
# Development: fast to build, easy to debug
g++ -std=c++17 -O0 -g -Wall -Wextra main.cpp -o main_debug
# Release: optimised for speed, no debug symbols
g++ -std=c++17 -O3 -Wall -Wextra main.cpp -o main_release
# CI/CD: strictest possible checking
g++ -std=c++17 -O2 -Wall -Wextra -Werror -fsanitize=address main.cpp -o main_ci
Those three lines serve three different purposes; do not mix them up. The debug build keeps
-O0 because optimisation deletes and reorders statements, which makes the debugger jump
around unpredictably. The release build drops -g to keep the file small. The CI build
adds -Werror (every warning becomes an error) and -fsanitize=address β much
slower to run, but it catches memory bugs while the tests are running.
7. Your first program in detail: hello_cpp.cpp
Let's go carefully through the Hello World program below and see how C++ code works, step by step:
Create the file hello_cpp.cpp
#include <iostream>
#include <string>
// A namespace of our own
namespace Engine {
void printV8Info() {
// std::string is a safe, self-managing string type - unlike a raw C char array
std::string engine = "Google V8";
std::cout << engine << " is written mostly in C++, which is why it can run JS this fast!" << std::endl;
}
}
int main() {
// std::cout lives in namespace std, and writes to the terminal
std::cout << "Welcome to the C++ series on js-tools.org!" << std::endl;
// Call the function from namespace Engine
Engine::printV8Info();
return 0;
}
Line by line
Lines 1-2: including headers
-
#include <iostream>: loads the standard C++ library needed forstd::cout(writing to the terminal) andstd::endl(a newline). -
#include <string>: loads the safe string typestd::string, used on line 8.
Lines 4-11: defining a namespace
A namespace is a separate "name space" that prevents clashes when two libraries define functions with
the same name. Here we create the namespace Engine containing the function
printV8Info(). To call it from main we have to use the prefix:
Engine::printV8Info().
Line 13: the main() function
Every C++ program starts at main(). The int return type means the program
returns an exit code to the operating system:
return 0;: the program finished successfully.return 1;(or any other value): something went wrong.
Line 15: std::cout (standard output)
std::cout << "..." << std::endl; means "write this text to the terminal and
start a new line". The << operator is called the stream insertion operator.
Compiling and running it
Once you have created hello_cpp.cpp, compile it:
# Compile against the C++17 standard
g++ -std=c++17 -Wall -Wextra hello_cpp.cpp -o hello_cpp
# Run it
./hello_cpp
# Expected output:
# Welcome to the C++ series on js-tools.org!
# Google V8 is written mostly in C++, which is why it can run JS this fast!
What each part of that command means:
g++: the C++ compiler.-std=c++17: use the C++17 standard.-Wall -Wextra: turn on the warnings, so mistakes surface early.hello_cpp.cpp: the source file to compile.-o hello_cpp: the name of the resulting executable.
std::cout rather than plain cout, when you have not
declared using namespace std;?
Download the lesson's sample source
You can download the complete sample C++ file for this lesson and practise with it directly on your own machine.
Download hello_cpp.cpp
Comments