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.
πŸ“Œ It is fine not to follow all of that yet
Phrases like "allocating memory", "class" and "template" that just appeared above each have a whole lesson of their own further along. You do not need to understand them in this lesson β€” you only need to know that these are the things C++ hands to you, and that this is why it is still chosen for the systems that demand the most speed. Lesson 1's job is to get your machine compiling C++ and running its first program.

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):

Terminal
# 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:

Terminal
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:

Terminal
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 apt in 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 .exe that runs without WSL. The trade-off is that libraries are installed with pacman rather than apt.
PowerShell / MSYS2
# 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:

Terminal
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Then install the common C++ libraries:

Terminal
# 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:

Terminal
# 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

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

Terminal
# 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

Terminal
# 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

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 for std::cout (writing to the terminal) and std::endl (a newline).
  • #include <string>: loads the safe string type std::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:

Terminal
# 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.
πŸ“ Check your understanding β€” Lesson 1
Why should you write 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