Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Warning

Warning! This is not complete in the slightest and is only just a placeholder for now.

This book is an introduction to C++ for those who have already completed the 10-part series introducing the C language found here: SANS Intro to C. If you have not completed that series, no worries, this series will cover some of those foundational topics. We assume you are comfortable with C fundamentals: variables, pointers, functions, and basic use of the preprocessor and compiler. If not, you will be by the end of this series.

Audience and focus

  • Prerequisites: Completion of the introductory C series.
  • Platform: The material and accompanying code are aimed at Windows. You will use the Windows SDK and Visual Studio (or compatible tooling) to build and run the examples.
  • Goal: To move from C to C++ in a structured way, using modern C++ (C++20/23) where it helps clarity and safety.

The companion code

All example code lives in the IntroToCpp Visual Studio solution:

  1. Open IntroToCpp.sln in Visual Studio (or open the solution folder in your IDE).
  2. Each chapter corresponds to one or more projects in the solution (e.g. FirstModule, PointersAndReferences).
  3. Set the project you want as the startup project, then build and run (F5 or Ctrl+F5).

Building the solution will compile the relevant C++ files; some projects use C++20 modules and the standard library as a module (import std;), so ensure your project is configured for C++20 or later.

GitHub Pages and GitHub Classroom

  • GitHub Pages: This mdBook can be built to static HTML and hosted on Intro to CPP, so the guide is available online.
  • GitHub Classroom: This series will be used with the GitHub Classroom with the classroom link here: Cpp Class Invite Link. The classroom is entirely optional but it will allow you to better participate and follow along with the exercises. The chapter order and project names in the book are chosen so that assignments can reference specific chapters and projects (e.g. “Complete the RAII project from Chapter 7”).

How to use this book

Chapters are ordered from basic to advanced. Each chapter introduces concepts and shows short code snippets that illustrate one idea at a time. When a snippet comes from the companion code, the project path is noted so you can open and run the full program. We recommend reading in order and building the corresponding projects as you go.

Hello C++

This chapter gets you to a minimal C++ program: one that uses the C++20 standard library module and prints a line of text. If you can build and run this, your environment is ready for the rest of the book.

Your first C++ program

In modern C++ with modules, the program can be very small. Here is the full program from the FirstModule project (IntroToCpp/FirstModule/main.cpp):

import std;

int main()
{
	std::println("{}:{}: hello!", __FUNCTION__, __LINE__);
}
  • import std; — Imports the C++20 standard library as a module. Your project must be set up to use the standard library module (e.g. MSVC with /std:c++20 or later and the std module enabled).
  • int main() — Program entry point, same idea as in C.
  • std::println("{}:{}: hello!", __FUNCTION__, __LINE__); — Prints a line. The {} placeholders are filled by the following arguments. __FUNCTION__ and __LINE__ are preprocessor macros that expand to the current function name and line number, so you get output like main:5: hello!.

Run the FirstModule project from IntroToCpp/FirstModule/ to see this in action. Once this builds and runs, you’re ready for variables, pointers, and references in the next chapter.

Variables, Pointers, and References

If you’re coming from C, you already know variables and pointers. In C++, references give you a way to pass or refer to an object without copying it and without using pointer syntax. This chapter contrasts variables, pointers, and pass-by-value vs pass-by-reference using the PointersAndReferences project.

Variables and pointers

A variable holds a value. A pointer is a variable that holds the address of another object. The address-of operator & gives you the address of an object.

From IntroToCpp/PointersAndReferences/main.cpp:

// a basic variable
DWORD theAnswer{ 42 };

// a basic pointer to variable
PDWORD ptrTheAnswer{ &theAnswer };

std::println(
	"{}: theAnswer: {}, it's address: {:p}, ptrTheAnswer: {:p} \n",
	__FUNCTION__,
	theAnswer,
	(PVOID)&theAnswer,
	(PVOID)ptrTheAnswer
);

Here theAnswer is a variable (value 42), and ptrTheAnswer is a pointer that holds the address of theAnswer. The { 42 } and { &theAnswer } are brace-initialization. On Windows, DWORD and PDWORD are the usual 32-bit type and pointer-to-DWORD; the cast to (PVOID) is used so the format specifier can print the address.

Pass-by-value (a copy)

When you pass an argument by value, the function receives a copy. Changes inside the function do not affect the original.

Declaration in ByValue.hpp:

DWORD ModifyByValue(DWORD dwByValue);

Definition in ByValue.cpp:

DWORD ModifyByValue(DWORD dwByValue)
{
	dwByValue += 42UL;
	return dwByValue;
}

The parameter dwByValue is a copy. Adding 42 to it only changes the copy; the caller’s variable is unchanged. The function returns the modified value so the caller can use it if needed.

Pass-by-reference (no copy)

When you pass by reference, the parameter is an alias for the original object. Changes inside the function affect the original. In C++ you use & after the type to denote a reference.

Declaration in ByReference.hpp:

DWORD ModifyByReference(DWORD& dwByReference);

Definition in ByReference.cpp:

DWORD ModifyByReference(DWORD& dwByReference)
{
	dwByReference += 42UL;
	return dwByReference;
}

Here dwByReference is a reference to the caller’s DWORD. Adding 42 to it changes the caller’s variable. No pointer syntax is needed at the call site.

Seeing the difference in main

In main, the same variable is passed to both functions:

auto retval = ModifyByValue(theAnswer);
// theAnswer is still 42 here; retval is 84

retval = ModifyByReference(theAnswer);
// theAnswer is now 84; retval is 84

After ModifyByValue(theAnswer), theAnswer is unchanged. After ModifyByReference(theAnswer), theAnswer has been updated. For large objects or when you want the function to modify the argument, passing by reference is usually the better choice.

You can build and run the PointersAndReferences project to see the before/after values printed for both styles.

Strings in C++

C++ provides std::string for dynamic character sequences, plus std::wstring for wide characters and std::string_view for non-owning views. This chapter uses the Strings and STD_strings projects to cover string types, lifecycle, parsing, and a simple flag map.

std::string lifecycle: empty, size, capacity

From IntroToCpp/Strings/main.cpp, an empty string still has a size and capacity managed by the implementation:

std::string name{};

if (name.empty())
{
	std::println("{}:{} the string is empty at this point", __FUNCTION__, __LINE__);
}
std::println("{}:{} the variable's size: {}, the variable's capacity: {}", __FUNCTION__, __LINE__, name.size(), name.capacity());

name = "Jonathan";
std::println("{}:{} the string finally has something: {}", __FUNCTION__, __LINE__, name);
std::println("{}:{} the variable's size: {}, the variable's capacity: {}", __FUNCTION__, __LINE__, name.size(), name.capacity());

name.clear();
if (name.empty())
{
	std::println("{}:{} doing name.clear() makes the string empty again", __FUNCTION__, __LINE__);
}
  • empty() — Returns whether the string has no characters.
  • size() — Current number of characters.
  • capacity() — How many characters the string can hold before reallocating.
  • clear() — Empties the string (size becomes 0; capacity may remain).

Splitting a string with ranges (StringSplitter)

The Strings project parses a line by splitting on spaces using C++20 ranges. From ParsingUtils.hpp and ParsingUtils.cpp:

// ParsingUtils.hpp
std::vector<std::string> StringSplitter(const std::string& theLine);
// ParsingUtils.cpp
std::vector<std::string> StringSplitter(const std::string& theLine)
{
	std::vector<std::string> values{};

	for (auto&& token : std::views::split(theLine, ' '))
	{
		values.emplace_back(token.begin(), token.end());
	}

	return values;
}

std::views::split(theLine, ' ') yields subranges; each is converted to a std::string and pushed into the vector. In main you can then iterate over the tokens:

std::string args{"this is a command line test for arg parsing"};
auto tokens{ StringSplitter(args) };
for (auto& token : tokens)
{
	std::println("{}: token: {}", __FUNCTION__, token);
}

std::string, std::wstring, and std::string_view

From IntroToCpp/STD_strings/main.cpp:

std::string szCourse{ "ANSI: Welcome to class!" };
std::wstring wszCourse{ L"UNICODE: Welcome to class!" };

std::cout << szCourse << std::endl;
std::wcout << wszCourse << std::endl;
std::println("{}", szCourse);
  • std::string — Narrow (char) string; works with std::cout and std::println.
  • std::wstring — Wide (wchar_t) string; use std::wcout. Note: std::println with the usual format does not accept wide strings directly.
  • std::string_view — A non-owning view over a contiguous sequence of characters. Good for function parameters when you don’t need to own or modify the string.

A function that takes std::string_view can accept std::string or string literals without copying:

void DumpString(std::string_view message)
{
	std::string formatted{ message.substr(0, 4) };
	std::println("extracting first 4 letters from: {} \n\t{}", message, formatted);
}
// Call: DumpString(szCourse);  or  DumpString("does this thing work?");

Tokenizing with std::istringstream

Tokenizing by whitespace can be done with an input stream:

std::vector<std::string> Tokenize(std::string& message)
{
	std::istringstream iss(message);
	std::string word{};
	std::vector<std::string> tokens{};

	while (iss >> word)
	{
		tokens.push_back(word);
	}
	return tokens;
}

The stream operator >> reads space-separated words into word; each is pushed into tokens.

Parsing flags into a map

The STD_strings example builds a map of flags to optional values (e.g. /file somefile.txt, /verbose with no value). Simplified structure:

std::unordered_map<std::string, std::string> MakeFlagsVales(const std::vector<std::string>& tokenVector)
{
	std::unordered_map<std::string, std::string> flagMap{};

	for (size_t i = 0; i < tokenVector.size(); ++i)
	{
		const auto& token{ tokenVector.at(i) };

		if (token.starts_with("/") || token.starts_with("-") || token.starts_with("--"))
		{
			std::string flag{ token };
			std::string value{};
			// If next token exists and is not another flag, use it as the value
			if (/* next is not a flag */)
				value = tokenVector.at(++i);
			flagMap[flag] = value;
		}
	}
	return flagMap;
}

Structured bindings when iterating the map

To print each flag and its value, C++17 structured bindings keep the loop readable:

void DumpFlagsValues(std::unordered_map<std::string, std::string>& flagMapping)
{
	for (const auto& [flag, value] : flagMapping)
	{
		std::println("{}: {}", flag, (value.empty() ? "(no value)" : value));
	}
}

[flag, value] binds to the key and value of each map element. Run the Strings and STD_strings projects to see these operations in action.

Output and Formatting

C++20’s std::println uses format strings similar to std::format: placeholders like {} are replaced by formatted arguments. This chapter shows alignment, fill, width, numeric options, and how to format pointers and runtime-built format strings using the STD_println and DynamicFormatArgs projects.

Alignment and width

From IntroToCpp/STD_println/main.cpp:

std::println("using alignment with 10 spaces... ");
std::println("\t|{:>10}|", "right");   // Right-aligned
std::println("\t|{:<10}|", "left");    // Left-aligned
std::println("\t|{:^10}|", "center");  // Centered
  • {:>10} — Right-align in a field of width 10.
  • {:<10} — Left-align in a field of width 10.
  • {:^10} — Center in a field of width 10.

Fill character

You can specify a fill character before the alignment character:

std::println("\t|{:*>10}|", "right");   // Right-aligned, filled with '*'
std::println("\t|{:-<10}|", "left");    // Left-aligned, filled with '-'
std::println("\t|{:.^10}|", "center");   // Centered, filled with '.'

So {:*>10} means “fill with *, then right-align in width 10.”

Numeric alignment and signed display

unsigned long number = 42;
std::println("\tRight-aligned: {:>10}", number);
std::println("\tLeft-aligned: {:<10}", number);

unsigned long positive = 42;
int negative = -42;
std::println("\tPositive: {:+}", positive);  // Always show '+' for positive
std::println("\tNegative: {:+}", negative);

The + in {:+} forces the sign to be shown for both positive and negative numbers.

Dynamic width and precision

Width and precision can be taken from arguments instead of being literal:

unsigned long width = 10;
double pi = 3.141592653589793;
std::println("{:{}.{}f}", pi, width, 4);  // Width 10, precision 4

Here the first {} is for pi, the next two supply width and precision for the floating-point format.

Formatting pointers with std::vformat

std::println does not accept pointer arguments in the usual way for hex output. To format a pointer (e.g. as hex), build the string with std::vformat and std::make_format_args, then print it. From DumpPointers() in IntroToCpp/STD_println/main.cpp:

unsigned long course{ 670 };

std::string templateFields{ "{}: 0x{:x}" };

// Use uintptr_t for the pointer value to avoid format/runtime issues
auto pointer{ std::bit_cast<uintptr_t>(&course) };

std::string finalFields{
	std::vformat(
		templateFields,
		std::make_format_args(__FUNCTION__, pointer)
	)
};

std::println("{}", finalFields);
  • std::vformat(templateFields, std::make_format_args(...)) — Formats the template string with the given arguments and returns a std::string.
  • Casting the address to uintptr_t (here via std::bit_cast) gives an integer type that the format layer can format as hex ({:x}) without undefined behavior. Using the raw pointer in the format API can cause runtime errors on some implementations.

Alternatively, for simple pointer output you can use streams:

std::cout << "the address of course is: 0x" << std::hex << &course << std::endl;

Runtime format string (DynamicFormatArgs)

When the format string itself is built at runtime (e.g. from user input or configuration), use std::vformat with std::make_format_args:

From IntroToCpp/DynamicFormatArgs/main.cpp:

std::string temp{
	"this {} is just {} vformatted strings \n"
};

std::string szFinal{
	std::vformat(
		temp,
		std::make_format_args(argv[0], argv[1])
	)
};

std::cout << szFinal;

Here temp is the format string and the arguments are filled in from argv. This pattern is useful whenever the format template is not a compile-time constant.

Build and run STD_println and DynamicFormatArgs to see all of these formatting options in action.

Namespaces and Organizing Code

Namespaces help you group names and avoid collisions with other code. C++ also has enum class for type-safe enumerations. This chapter uses the namespaces project to show nested namespaces, an enum class, and a simple class interface declared in a header.

Nested namespaces and enum class

From IntroToCpp/namespaces/tables.hpp:

#pragma once

#include <string>
#include <vector>

#define TABLE_SEPARATOR "-----"
#define TABLE_MAX_SIZE_DEFAULT (64)

namespace utils
{
	namespace ui {
		enum class TableAlign
		{
			LEFT,
			RIGHT
		};
  • namespace utils { namespace ui { ... } } — Nested namespaces. You can also write namespace utils::ui { ... } in C++17.
  • enum class TableAlign — A scoped enumeration. Values are referred to as TableAlign::LEFT and TableAlign::RIGHT, and they do not implicitly convert to integers, which reduces bugs.

A class in a namespace

The same header declares a Table class inside utils::ui:

		class Table
		{
		private:
			std::vector<std::vector<std::string>> headers;
			std::vector<utils::ui::TableAlign> column_align;
			std::vector<std::vector<std::vector<std::string>>> data;
			std::vector<std::vector<std::string>> current_line;

			bool border_top = true;
			bool border_bottom = true;
			bool border_left = true;
			bool border_right = true;
			bool interline = false;
			bool header_interline = true;
			bool corner = true;
			uint32_t margin_left;
			uint32_t cell_max_size = TABLE_MAX_SIZE_DEFAULT;
		public:
			explicit Table();

			void add_header_line(std::string header, utils::ui::TableAlign align = TableAlign::LEFT);
			void add_header_multiline(std::initializer_list<std::string> header, utils::ui::TableAlign align = TableAlign::LEFT);
			void add_item_line(std::string item);
			void add_item_multiline(std::initializer_list<std::string> list);
			void add_item_multiline(std::vector<std::string> list);

			void set_margin_left(uint32_t margin_left);
			void set_cell_max_size(uint32_t max_size);
			void set_corner(bool show) { corner = show; }
			void set_interline(bool show) { interline = show; }
			void set_header_interline(bool show) { header_interline = show; }
			void set_border_left(bool show) { border_left = show; }
			void set_border_top(bool show) { border_top = show; }
			void set_border_right(bool show) { border_right = show; }
			void set_border_bottom(bool show) { border_bottom = show; }
			void set_border(bool show) { border_bottom = show; border_left = show; border_right = show; border_top = show; }

			void new_line();
			void render(std::ostream& out);
		};
	}
}

Takeaways:

  • explicit Table(); — The constructor is explicit so it cannot be used for implicit conversions.
  • std::initializer_list<std::string> — Allows callers to pass a brace-enclosed list of strings, e.g. add_header_multiline({"Col1", "Col2"}, TableAlign::LEFT).
  • Inline member functions — e.g. void set_corner(bool show) { corner = show; } are defined in the header.
  • Default argumentTableAlign align = TableAlign::LEFT so callers can omit the second argument.

The implementation lives in tables.cpp, where member functions are qualified with the namespace: utils::ui::Table::Table(), void utils::ui::Table::add_header_line(...), etc. The implementation uses a helper utils::strings::utf8_string_size; in a classroom or full repo you would either provide that function in a utils::strings namespace or simplify the table logic so the book and code stay in sync.

Using the namespace in main

From IntroToCpp/namespaces/main.cpp:

import std;

int main()
{
	std::println("{}:{}: hello!", __FUNCTION__, __LINE__);
}

In a fuller example you would construct utils::ui::Table, call add_header_line, add_item_line, new_line, and render to build and print a table. The header snippet above shows how namespaces and an enum class organize the public interface of a small utility class.

RAII

RAII (Resource Acquisition Is Initialization) is a core C++ idiom: you acquire a resource when you construct an object and release it when the object is destroyed. That way you don’t forget to free the resource, and the compiler guarantees cleanup even when exceptions are thrown. This chapter uses the RAII and GetProcAddr-RAII projects to illustrate RAII for raw memory and for Windows API handles.

What RAII means

From the comments in IntroToCpp/RAII/main.cpp:

  • The moment you acquire a resource, you initialize an object with it (e.g. in the constructor).
  • The resource’s release should be automatic (in the destructor), so you don’t have to remember to free it.
  • The resource can be memory, file handles, process handles, mutexes, etc.
  • Whether the code succeeds or fails (including by throwing), the destructor runs when the object goes out of scope, so the resource is freed.

Example: RAII for a dynamic array (DoubleArrayRAII)

From IntroToCpp/RAII/main.cpp:

class DoubleArrayRAII final
{
public:
	// ctor: acquire the resource
	explicit DoubleArrayRAII(size_t size) : m_resource{ new double[size] } {}

	// dtor: release the resource
	~DoubleArrayRAII()
	{
		std::println("freeing memory");
		delete[] m_resource;
	}

	// No copying: two objects would point to the same resource
	DoubleArrayRAII(const DoubleArrayRAII&) = delete;
	DoubleArrayRAII& operator=(const DoubleArrayRAII&) = delete;

	// array subscript operator
	double& operator[](size_t index) noexcept { return m_resource[index]; }
	const double& operator[](size_t i) const noexcept { return m_resource[i]; }

	double* get() const noexcept { return m_resource; }

	// Optional: hand ownership to the caller; RAII object no longer frees
	double* release() noexcept
	{
		double* result = m_resource;
		m_resource = nullptr;
		return result;
	}
private:
	double* m_resource;
};
  • Constructor — Allocates new double[size] and stores the pointer in m_resource.
  • Destructor — Frees the array with delete[] m_resource. This runs when the object goes out of scope or is destroyed, so the memory is always released.
  • Deleted copy constructor and assignment — Prevents two DoubleArrayRAII objects from sharing the same pointer; otherwise both would try to delete it.
  • operator[] — Provides array-like access.
  • release() — Transfers ownership to the caller: returns the pointer and sets m_resource to nullptr so the destructor no longer deletes it. Use when you need to pass ownership out of the RAII object.

Usage:

DoubleArrayRAII values{ 5 };
for (size_t i{}; i < 5; i++)
{
	values[i] *= 2;  // If something throws here, the dtor still runs and frees memory
}

RAII for Windows API: LoadLibrary / FreeLibrary

The GetProcAddr-RAII project wraps LoadLibrary, GetProcAddress, and FreeLibrary so the DLL is always freed. From IntroToCpp/GetProcAddr-RAII/main.cpp:

class ProcedurePtr
{
public:
	explicit ProcedurePtr(FARPROC ptr) : _funcptr(ptr) {}

	template <typename TEMP, typename = std::enable_if_t<std::is_function_v<TEMP>>>
	operator TEMP* () const
	{
		return reinterpret_cast<TEMP*>(_funcptr);
	}

private:
	FARPROC _funcptr;
};

class LoadLibHelper
{
public:
	explicit LoadLibHelper(std::string_view DllName) : _modHandle(LoadLibraryA(DllName.data())) {}
	~LoadLibHelper() { FreeLibrary(_modHandle); }

	ProcedurePtr operator[](std::string_view funcName) const
	{
		return ProcedurePtr(GetProcAddress(_modHandle, funcName.data()));
	}

private:
	HMODULE _modHandle;
};
  • LoadLibHelper — Constructor calls LoadLibraryA; destructor calls FreeLibrary. When a LoadLibHelper goes out of scope, the DLL is unloaded.
  • operator[](std::string_view funcName) — Returns a ProcedurePtr that wraps the result of GetProcAddress. The caller can use it to get a function pointer.
  • ProcedurePtr — Holds a FARPROC. The template conversion operator operator TEMP* () allows casting to a specific function pointer type; std::enable_if_t<std::is_function_v<TEMP>> restricts it to function types so it doesn’t match pointer-to-object types.

Example usage pattern (conceptually):

LoadLibHelper lib("some.dll");
auto func = lib["SomeFunction"];  // ProcedurePtr; can convert to void (*)(), etc.
// When lib goes out of scope, FreeLibrary is called automatically.

Together, RAII and GetProcAddr-RAII show the same idea: acquire in the constructor, release in the destructor, and avoid copying when the resource is not shareable. Build and run these projects to see RAII in action.

Lambdas

A lambda is an anonymous callable object: you define a small function inline, often to pass to an algorithm or to capture local variables. This chapter uses the Lambdas project to show lambda syntax, captures, mutable, and using lambdas with the standard library.

Lambda syntax

A lambda has three parts:

  • [] — Capture list (what from the surrounding scope the lambda can use).
  • () — Parameter list (like a function).
  • {} — Body (the code that runs when the lambda is called).

So a minimal lambda looks like [](){}. It can be written on one line or split across lines.

Generic lambda with std::find_if

From IntroToCpp/Lambdas/main.cpp, a lambda is used as the predicate for std::find_if:

constexpr std::array<std::string_view, 4> users{ "Admin", "Guest", "sec670", "System" };

auto granted = std::find_if(
	users.begin(),
	users.end(),
	[](std::string_view user) { return user.find("System") != std::string_view::npos; }
);

if (granted == users.end())
	std::println("user not found");
else
	std::println("user found: {}", *granted);
  • [] — Empty capture: the lambda doesn’t use any local variables from the enclosing scope.
  • (std::string_view user) — One parameter; find_if passes each element as user.
  • { return user.find("System") != std::string_view::npos; } — Returns true when the string contains "System".

So the lambda is a small predicate that says “this element is the one we want.”

Value capture [previous]

Capturing by value copies the variable into the lambda:

int previous{ 10 };

auto lamb = [previous](auto nextClass) { return 5 + nextClass + previous; };

std::println("lamb is: {}", lamb(100));   // 5 + 100 + 10 = 115
std::println("previous after lamb(100) is: {}", previous);  // still 10

previous is read-only inside the lambda (by value). Changing it inside the lambda would not be allowed unless the lambda is mutable.

Mutable capture

If you want to modify the captured copy, mark the lambda mutable:

int previous = 10;

auto lamb = [previous](auto nextClass) mutable { return 5 + nextClass + ++previous; };

std::println("previous is: {}", previous);
std::println("lamb(100) is: {}", lamb(100));
std::println("previous after lamb(100) is: {}", previous);  // still 10; the copy inside the lambda changed

Inside the lambda, previous is a copy that can be modified; the outer previous is unchanged.

Reference capture [&] and [&previous]

Capturing by reference lets the lambda see and modify the original variable:

int previous = 10;
int next = 20;

auto lamb = [&]() {
	++previous;
	++next;
};

lamb();
lamb();
std::println("previous after lamb() is: {}", previous);  // 12
  • [&] — Capture all local variables by reference. Any use of previous or next inside the lambda refers to the outer variables.

To capture only one variable by reference:

auto lamb = [&previous]() {
	++previous;
	// ++next;  // error: next not in capture list
};

Parameters by reference

Lambda parameters can be references so the lambda can modify the argument:

std::string message{ "I changed the message!" };
int age{ 40 };

auto lamb = [](std::string& message, int& age) {
	std::println("the message is: {}", message);
	message = "woot!";
};

lamb(message, age);
std::println("did the message change? {}", message);  // "woot!"

Capturing [this]

In a member function, a lambda can capture this to access the object’s members (including private ones):

class TheLamb
{
public:
	void Hello()
	{
		auto lambda = [this]() { SayHello(); };
		lambda();
	}

private:
	void SayHello()
	{
		std::println("Hello there! I'm this lambda :) ");
	}
};

[this] gives the lambda access to the current object so it can call SayHello(). Use [this] only when the lifetime of the object is guaranteed for as long as the lambda might be called.

Build and run the Lambdas project to see all of these forms in action.

Compile-Time Programming

constexpr lets you mark variables and functions so they can be evaluated at compile time. That enables constant expressions, better optimization, and in some cases compile-time data (e.g. string obfuscation). This chapter uses the ConstExpr project and helpers.hpp to show constexpr variables and functions, if constexpr, and type traits.

constexpr variables

A constexpr variable must be initialized with a constant expression and can be used where the language requires a compile-time constant:

constexpr std::string_view __EXE__{ "ConstExpr" };
std::println("{} was built on {}", __EXE__, __TIMESTAMP__);

__EXE__ is a compile-time constant string view. __TIMESTAMP__ is a preprocessor macro that expands to the build time.

constexpr functions

A constexpr function can be evaluated at compile time when its arguments are constant. From IntroToCpp/ConstExpr/helpers.hpp:

constexpr int FactorialLoop(int n)
{
	int result{ 1 };

	for (int i{ 2 }; i <= n; ++i)
	{
		result *= i;
	}

	return result;
}

When you call it with a constant, the result can be computed at compile time:

constexpr int result{ FactorialLoop(5) };
std::println("factorial loop of 5 is {}", result);  // 120

The same function can still be used at runtime with non-constant arguments.

if constexpr and type traits

if constexpr chooses a branch at compile time. The other branch is discarded, so it can contain code that would be invalid for some types. Combined with type traits you can specialize behavior per type:

From IntroToCpp/ConstExpr/helpers.hpp:

template <typename TYPE>
constexpr TYPE add(TYPE a, TYPE b)
{
	if constexpr (std::is_integral_v<TYPE>)
	{
		return a + b;
	}
	else
	{
		return a * b;
	}
}
  • std::is_integral_v<TYPE> — True for integral types (int, char, etc.), false for floating-point and other types.
  • For integral types the compiler uses the return a + b; branch.
  • For other types (e.g. double) it uses the return a * b; branch.

So add(5, 11) is 16, and add(5.0, 11.0) is 55.0. The choice is made at compile time, with no runtime overhead.

Compile-time string obfuscation (optional)

The ConstExpr project also includes a macro that obfuscates a string at compile time so the literal does not appear plainly in the binary. The idea is to store an encrypted form in a constexpr-friendly structure and decrypt at runtime. From helpers.hpp, simplified:

  • EncryptedString<N> — A small struct that holds encrypted character data and a key, with a constexpr constructor that XORs the string at compile time.
  • STR_OBFUSCATE("text") — Produces an EncryptedString that decrypts to "text" when used (e.g. via operator const char* that calls decrypt()).

In main:

auto sec670{ STR_OBFUSCATE("SEC670 should be hidden") };
std::cout << sec670 << std::endl;

The string "SEC670 should be hidden" appears only once in the source; in the compiled binary the stored form is obfuscated. This pattern is useful when you want to avoid plain-string literals in the binary while still using normal string literals in code.

Build and run the ConstExpr project to see constexpr, FactorialLoop, add with if constexpr, and the obfuscation in action.

Attributes

Attributes add metadata or hints for the compiler: they can enforce usage (e.g. “don’t ignore the return value”), mark functions that never return, or deprecate old APIs. This chapter uses the Attributes project (Useful.hpp, Useful.cpp, main.cpp) to show [[nodiscard]], [[noreturn]], and [[deprecated]], plus a brief note on __assume.

[[nodiscard]]

If a function’s return value is important (e.g. an error code or a newly allocated resource), you can mark it with [[nodiscard]]. The compiler will warn when the return value is discarded.

From IntroToCpp/Attributes/Useful.hpp:

[[nodiscard]]
unsigned long ResolveLastError(const unsigned long& errorCode);

[[nodiscard("You must check the return value")]]
unsigned long ResolveLastError(const int& errorCode);
  • The first overload uses [[nodiscard]] with no message.
  • The second uses [[nodiscard(“You must check the return value”)]] so the warning includes a custom message.

If callers write ResolveLastError(5); without using the result, the compiler can emit a warning. Use [[nodiscard]] on functions whose return value should always be checked.

[[noreturn]] and [[deprecated]]

[[noreturn]] tells the compiler that the function never returns (e.g. it always throws or terminates). [[deprecated]] marks something as obsolete; the compiler can warn when it is used and optionally show a message.

From Useful.hpp:

[[noreturn, deprecated("This function is deprecated. Use the newer version.")]]
void DumpString(const std::string_view& strings);
  • [[noreturn]] — The compiler may assume that code after a call to DumpString is unreachable (useful for optimization and static analysis).
  • [[deprecated(“…”)]] — Calls to DumpString can produce a deprecation warning with the given message.

You can combine multiple attributes in one list, as shown. The implementation in Useful.cpp would typically not return (e.g. call std::terminate or throw); as written in the project it is a stub.

__assume (compiler hint)

In the Attributes implementation, __assume is used to tell the compiler something about the program so it can optimize (e.g. assume a path is never taken). From Useful.cpp:

unsigned long ResolveLastError(const unsigned long& errorCode)
{
	__assume(errorCode > 0);
	SetLastError(errorCode);
	std::println("{}: GetLastError: {}", errorCode, GetLastError());
	return 0;
}

unsigned long ResolveLastError(const int& errorCode)
{
	switch (errorCode)
	{
	case 1:
	case 2:
	case 3:
		std::println("{}: the case: {}", __FUNCTION__, errorCode);
		break;
	default:
		__assume(0);  // dead code; compiler may omit it
	}
	return 0;
}
  • __assume(condition) — A hint that the condition is true at that point; undefined behavior if it isn’t. Used here to document assumptions or unreachable code.
  • __assume(0) in default — Tells the compiler this branch is never taken, so it may omit the code. Use only when you are certain the branch is unreachable.

__assume is a compiler-specific extension (e.g. MSVC); it is not standard C++. Use it only where you need that kind of optimization hint.

Build and run the Attributes project to see how the compiler responds to [[nodiscard]] and [[deprecated]] in practice.

Modules

C++20 modules let you import the standard library and your own module units instead of relying only on #include. That can improve build times and isolate interfaces. This chapter uses FirstModule (which already uses import std) and ErrorModule with a custom errors module implemented in errors.ixx.

Importing the standard library

From the start of the book you’ve seen:

import std;

int main()
{
	std::println("{}:{}: hello!", __FUNCTION__, __LINE__);
}

import std; imports the C++ standard library as a module. Your project must be configured to build the standard library module (e.g. in Visual Studio, use a C++20 or later standard and the option that enables the std module). After that, you don’t need to #include most standard headers for the parts you use via the module.

Defining and exporting a custom module

A module is declared in a module unit (e.g. an .ixx file). The module name must be the first declaration; no #include or other code may appear before it.

From IntroToCpp/ErrorModule/errors.ixx:

// Module name must be first; no includes before this line
export module errors;

#include <Windows.h>

import std;

export std::string ResolveErrorCode(
	const std::string_view& Message,
	const DWORD ErrorCode
);

std::string ResolveErrorCode(
	const std::string_view& Message,
	const DWORD ErrorCode
)
{
	LPSTR messageBuffer{};
	std::string resolvedMessage{};

	DWORD retval{
		FormatMessageA(
			FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
			nullptr,
			ErrorCode,
			0,
			(LPSTR)&messageBuffer,
			0,
			nullptr
		)
	};

	if (!retval)
		return std::string{ "" };

	resolvedMessage = std::format("{} {}", Message, std::string(messageBuffer, retval));

	if (messageBuffer)
		LocalFree(messageBuffer);

	return resolvedMessage;
}
  • export module errors; — Declares this translation unit as the module named errors. It must be the first declaration.
  • export std::string ResolveErrorCode(...); — The function is exported, so any translation unit that imports errors can call it.
  • The implementation of ResolveErrorCode uses the Windows API FormatMessageA to turn an error code into a string, then returns a formatted message. The function is defined in the same file after the export declaration.

(Some setups require a pragma to allow #include <Windows.h> inside the module; the project uses #pragma warning(disable : 5244) for that.)

Importing and using the custom module

From IntroToCpp/ErrorModule/main.cpp:

import std;
import errors;

int main()
{
	std::println("testing with error code 5: {} ", ResolveErrorCode("Oops! ", 5));
	return 0;
}
  • import errors; — Makes the errors module’s exported names visible. No path or file extension is used; the build system maps the module name to the correct .ixx (or other module unit).
  • ResolveErrorCode("Oops! ", 5) — Calls the function exported from the errors module. The result is a string that combines the prefix and the system message for error code 5.

So the flow is: define a module in an .ixx (or other module source), export the interface you want, then import that module in other files. Build and run the ErrorModule project to see custom modules in action; FirstModule shows the standard library as a module.

Variadic and Generic Code

Parameter packs and variadic templates let you write functions that accept zero or more arguments in a type-safe way. With C++20 you can constrain the pack with concepts like std::convertible_to so only certain types are accepted. This chapter uses the ParameterPacks project to show a variadic function, pack expansion, and a type constraint.

Parameter pack basics

  • A parameter pack is declared with an ellipsis before the name, e.g. ...s. The pack can hold multiple arguments of (possibly different) types.
  • Expanding the pack means using it in a pattern that produces a comma-separated list; the pack name goes on the other side of the ellipsis, e.g. s....
  • You can only use a parameter pack in a context that expands it (e.g. passing to a function, building an initializer list, or a template argument list).

A constrained variadic function

From IntroToCpp/ParameterPacks/main.cpp:

void VariadicTemp1(std::convertible_to<std::string_view> auto&& ...s);

int main()
{
	std::println("{}: {} was built on {}", __FUNCTION__, __EXE__, __TIMESTAMP__);

	VariadicTemp1("hello", std::string{ "there" }, std::string_view{ "these are strings" });
}

void VariadicTemp1(std::convertible_to<std::string_view> auto&& ...s)
{
	for (auto thing : std::initializer_list<std::string_view>{ s... })
	{
		std::cout << thing << " ";
	}
	std::println("");
}
  • std::convertible_to<std::string_view> auto&& ...s — Each argument must be convertible to std::string_view. auto&& allows both lvalues and rvalues (forwarding reference for each element). …s is the parameter pack.
  • std::initializer_list<std::string_view>{ s... }s… expands the pack into the initializer list: each argument is converted to std::string_view and the list is built. The loop then iterates over those string views.

So the function accepts any number of arguments as long as each is convertible to std::string_view, and it prints them space-separated. The call passes a string literal, a std::string, and a std::string_view; all satisfy the constraint.

Why use a type constraint?

Without std::convertible_tostd::string_view, callers could pass types that don’t convert to string view, and errors would appear deep in the template or at runtime. The constraint gives clearer compile-time errors and documents the intended use. Other constraints (e.g. std::same_as<DWORD> for a pack of DWORDs) can be used the same way.

Build and run the ParameterPacks project to see variadic templates and pack expansion in action.

Windows-Specific Topics

This chapter touches on Windows-only features used in the companion code: calling conventions, pragma comment for linking, Win32 API usage (e.g. CreateFile/CloseHandle), RPC enumeration with RAII-style cleanup, and native application entry. The snippets are from CallMeMaybe, RPCEnum, and NativeApps/NativeHello. All require the Windows SDK and appropriate link libraries.

Calling conventions

On Windows, functions can use different calling conventions that define how arguments and the return value are passed. Two you may see:

  • __cdecl — The default for C and many C++ functions (e.g. main). Caller cleans the stack.
  • __fastcall — Allows passing some arguments in registers; often used for performance-sensitive or internal APIs.

From IntroToCpp/CallMeMaybe/main.cpp:

int _cdecl main(INT argc, PCHAR argv[], PCHAR envp[])
{
	// ...
}

DWORD __fastcall _Use_decl_annotations_
ThisIsFastCall(INT& First, INT& Second, INT& Third)
{
	return First + Second + Third;
}
  • _Use_decl_annotations_ — Tells the compiler to apply SAL (Source Annotation Language) annotations so static analysis can better understand parameters (e.g. that they are read/written).

You usually only need to specify a convention when calling or implementing Windows API callbacks or when interfacing with code that expects a specific convention.

Linking a library via #pragma comment

Instead of adding a library to the project settings, you can request it in source:

#pragma comment(lib, "Shlwapi.lib")

The linker will link against Shlwapi.lib. The CallMeMaybe project uses this for StrToIntExA and similar functions. Other examples in the solution use #pragma comment(lib, "Rpcrt4.lib") for RPC.

Win32 API: CreateFile and CloseHandle

From CallMeMaybe, a minimal pattern for creating a file and closing the handle:

HANDLE hLogFile = INVALID_HANDLE_VALUE;
std::string logFileName{ "LogFile.txt" };

hLogFile = CreateFileA(
	logFileName.c_str(),
	GENERIC_WRITE,
	NULL,
	nullptr,
	CREATE_NEW,
	FILE_ATTRIBUTE_NORMAL,
	HANDLE()
);

if (INVALID_HANDLE_VALUE == hLogFile)
{
	printf("CreateFile error: %d\n", GetLastError());
	return ERROR_FILE_INVALID;
}

CloseHandle(hLogFile);
  • CreateFileA — Opens or creates a file; returns a handle or INVALID_HANDLE_VALUE on failure.
  • CloseHandle — Releases the handle. In real code you would often wrap this in an RAII class (like GetProcAddr-RAII does for LoadLibrary/FreeLibrary) so the handle is always closed.

This is Windows-specific and requires the Windows headers and linkage to the appropriate system libraries.

RPC enumeration with RAII-style cleanup

The RPCEnum project uses the Windows RPC API to enumerate endpoints. The class holds RPC handles and frees them in the destructor so cleanup is automatic:

From IntroToCpp/RPCEnum/main.cpp:

#pragma comment(lib, "Rpcrt4.lib")

class RPCEnum
{
public:
	explicit RPCEnum(const std::string& server) : _targetServer(server) {}

	~RPCEnum()
	{
		if (stringBinding) RpcStringFreeW(&stringBinding);
		if (bindingHandle) RpcBindingFree(&bindingHandle);
		if (inquiryHandle) RpcMgmtEpEltInqDone(&inquiryHandle);
	}

	void QueryEndpoints()
	{
		status = RpcMgmtEpEltInqBegin(
			nullptr,
			RPC_C_EP_ALL_ELTS,
			nullptr,
			RPC_C_VERS_ALL,
			nullptr,
			&inquiryHandle
		);
		// ... loop with RpcMgmtEpEltInqNextW, RpcBindingToStringBindingW, etc.
	}

private:
	std::string _targetServer{};
	RPC_STATUS status{};
	RPC_EP_INQ_HANDLE inquiryHandle{};
	RPC_BINDING_HANDLE bindingHandle{};
	RPC_WSTR stringBinding{};
	RPC_WSTR annotation{};
	// ...
};

int main()
{
	std::string target{ "192.168.236.136" };
	auto RPC{ RPCEnum(target) };
	RPC.QueryEndpoints();
}

The destructor releases the RPC string binding, binding handle, and inquiry handle so you don’t have to remember to free them manually. The pattern is the same as in Chapter 7: acquire in the constructor (or in methods like QueryEndpoints), release in the destructor.

Native application entry point

A native Windows application (e.g. one that runs without the C runtime or as a minimal process) can use a different entry point. From IntroToCpp/NativeApps/NativeHello/main.cpp:

#include <Windows.h>
#include <winternl.h>

EXTERN_C_START

void NTAPI NtProcessStartup(PPEB peb)
{
	// native stuff here
}

EXTERN_C_END
  • NtProcessStartup — Entry point used when the executable is run as a native application (e.g. by the native loader or in special environments). The PPEB (Process Environment Block) is passed by the loader.
  • EXTERN_C_START / EXTERN_C_END — Ensure the function is linked with C linkage so the loader can find it by name without C++ name mangling.
  • NTAPI — Denotes the Windows NT calling convention.

This is a very low-level topic; you would use it only when building a native executable (e.g. with the Windows NT native subsystem). For normal desktop apps, main or wWinMain is the entry point.


All of the code in this chapter is Windows-specific and requires the Windows SDK and the correct project settings (include paths, link libraries, and possibly subsystem and entry-point settings for native apps). Build CallMeMaybe, RPCEnum, and NativeApps/NativeHello only in a Windows development environment.