With the environment set up and a first program compiled, the next thing to get solid is the foundation of modern C++. This lesson covers the core features: namespaces, type modifiers (const, constexpr, volatile), the auto keyword, iostream I/O, and safe type casting. These are what let you write C++ that is clean, safe and fast.

1. Namespaces: keeping names apart in a large project

A namespace lets you organise code into separate "name regions", so that names do not collide when you pull in several libraries. Both OpenGL and a graphics library you wrote yourself may define drawTriangle(); a namespace is what keeps them distinguishable:

namespaces.cpp
#include <iostream>

// A namespace of your own
namespace Graphics {
    void drawTriangle() {
        std::cout << "Graphics::drawTriangle" << std::endl;
    }
}

// A different library that happens to use the same function name
namespace Physics {
    void drawTriangle() {
        std::cout << "Physics::drawTriangle (collision shape)" << std::endl;
    }
}

// Nested namespace, C++17 syntax
namespace Graphics::Engine {
    void initRenderer() {
        std::cout << "Graphics::Engine::initRenderer" << std::endl;
    }
}

int main() {
    // The prefix is what makes the two drawTriangle() distinguishable
    Graphics::drawTriangle();
    Physics::drawTriangle();
    Graphics::Engine::initRenderer();
    return 0;
}

The thing to notice is that the two drawTriangle() functions have exactly the same name and still coexist, because the namespace prefix separates them. That is the whole reason namespaces exist.

⚠️ Never add anything to namespace std
You will find examples online that reopen namespace std to add a function to it. Do not copy them: the C++ standard states plainly in [namespace.std] that adding declarations or definitions to namespace std makes the behaviour of the whole program undefined.

What makes this far more dangerous than a syntax error: no compiler warns you. I tried both g++ and clang++ with -Wall -Wextra — both compiled cleanly and the program printed exactly what you would expect. It only breaks on the day you upgrade your standard library and the name you added collides with a real one inside std.

The one exception the standard allows is specialising an existing std template for your own type — std::hash<MyType>, for instance. Adding new functions is not covered by it.

Using directive & using declaration

Rather than writing std::cout every time, using pulls a name into the current scope. Be careful though: using namespace std; at global scope invites exactly the collisions namespaces were built to prevent:

using_directive.cpp
#include <iostream>

// using declaration: pull in ONE name only
using std::cout;
using std::endl;

int main() {
    cout << "Safer than using namespace std" << endl;

    // Scoped to this block only - the safest form
    {
        using std::cin;
        int x;
        cin >> x;
    }

    return 0;
}

Namespace alias

When a namespace name gets long (boost::asio::ssl::stream), you can give it a shorter alias:

namespace_alias.cpp
namespace fs = std::filesystem;  // Alias
namespace ba = boost::asio;      // Alias

// Now the call sites stay short
fs::path my_path = "/tmp/file.txt";
ba::io_context io;

2. Type modifiers: const, constexpr & volatile

These keywords control how data may change, and they let the compiler catch logic mistakes early.

const: a value that does not change

const says a variable cannot be reassigned after initialisation. It applies to variables, to pointers, and to class methods:

const_demo.cpp
int main() {
    // A const variable: cannot be reassigned after initialisation
    const int MAX_SIZE = 100;
    // MAX_SIZE = 200;      // ERROR: cannot assign to a const

    int x = 10, y = 20;

    // CONST POINTER: the pointer is fixed, the value it points at is not.
    // Read it right-to-left: "ptr is a const pointer to int".
    int* const ptr = &x;
    *ptr = 30;              // OK - writing through it is allowed
    // ptr = &y;            // ERROR: the pointer itself cannot be repointed

    // POINTER TO CONST: the opposite. The pointer moves, the value is read-only.
    // "to_const is a pointer to const int".
    const int* to_const = &x;
    // *to_const = 40;      // ERROR: cannot write through a pointer-to-const
    to_const = &y;          // OK - repointing is allowed

    return 0;
}

constexpr: computed at compile time

constexpr requires the value to be computed at compile time, which lets the compiler optimise around it. Useful for constants that take real work to derive:

constexpr_demo.cpp
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    // Evaluated at COMPILE time, not at run time
    constexpr int result = factorial(5);  // The compiler computes 5! = 120

    // A compile-time constant array
    constexpr int arr[] = {1, 2, 3, 4, 5};

    return 0;
}

volatile: a value that may change outside your program's control

volatile tells the compiler the value may change unexpectedly — from hardware, a signal handler, or another thread — so it must not be optimised into a register:

volatile_demo.cpp
// volatile is for hardware registers and memory written from outside
volatile int hardware_counter = 0;  // May change without this code touching it

// The compiler may not cache it in a register - every read hits memory
while (hardware_counter < 100) {
    // hardware_counter is re-read on every iteration
}

// const volatile: this code may not write it, but something else may
const volatile int* hw_ptr = &hardware_counter;

3. The auto keyword: letting the compiler work out the type

auto asks the compiler to deduce the type from the initialiser. It keeps code short, especially around STL containers:

auto_demo.cpp
#include <iostream>
#include <map>
#include <vector>

int main() {
    // auto with ordinary values
    auto x = 42;           // int
    auto name = "John";    // const char*
    auto pi = 3.14159;     // double

    // auto with STL containers
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Instead of: std::vector<int>::iterator it = numbers.begin();
    // just write:
    auto it = numbers.begin();

    // Range-based for loop with auto
    for (auto num : numbers) {
        std::cout << num << " ";  // auto deduces int here
    }

    // Map iterators get verbose fast - this is where auto really pays
    std::map<std::string, int> scores;
    for (const auto& [name, score] : scores) {  // Structured binding (C++17)
        std::cout << name << ": " << score << std::endl;
    }

    return 0;
}

Careful: auto is not always the right call

Convenient as it is, overusing auto makes code harder to read. Reach for auto when:

  • The type is obvious from the value (for example auto x = std::make_unique<MyClass>();).
  • You are dealing with iterators or complicated template types.
  • Do not use auto when the type is not obvious — it just hides information.

Range-based for and const auto&

Where auto earns its keep most is looping over a container. But how you write it decides whether the program copies your data or not — and with large objects that is a real difference, not a matter of taste:

range_for.cpp
#include <iostream>
#include <string>
#include <vector>

struct Report {
    std::string title;
    std::string body;   // imagine this is a few hundred KB
    Report(std::string t, std::string b) : title(std::move(t)), body(std::move(b)) {}
    Report(const Report& other) : title(other.title), body(other.body) {
        std::cout << "  [COPY " << title << "]\n";   // count every copy
    }
};

int main() {
    std::vector<Report> reports;
    reports.reserve(2);   // Without reserve the vector reallocates and copies once by itself
    reports.emplace_back("A", "...");
    reports.emplace_back("B", "...");

    std::cout << "auto (copies every element):\n";
    for (auto r : reports) {
        std::cout << "  read " << r.title << "\n";
    }

    std::cout << "const auto& (no copy):\n";
    for (const auto& r : reports) {
        std::cout << "  read " << r.title << "\n";
    }

    std::cout << "auto& (no copy, AND writable):\n";
    for (auto& r : reports) {
        r.title += "!";
    }
    for (const auto& r : reports) std::cout << "  " << r.title << "\n";

    return 0;
}
What it actually prints
auto (copies every element):
  [COPY A]
  read A
  [COPY B]
  read B
const auto& (no copy):
  read A
  read B
auto& (no copy, AND writable):
  A!
  B!

The [COPY …] lines appear only in the first loop. Writing auto r tells the compiler "give me a copy" — and with a Report holding a few hundred KB, every iteration is a fresh allocation and a full copy, for no reason at all.

Three forms, three distinct purposes:

  • const auto& — the sensible default. No copying, and the compiler stops you if you modify an element by accident.
  • auto& — when you want to modify elements in place.
  • auto (no &) — only when you genuinely need a copy to change without touching the original, or the element is something small like an int.

4. iostream & I/O basics: reading and writing safely

C++ offers iostream as a safer replacement for C's scanf/printf. A stream is a two-way flow of data between your program and a device (terminal, file, network).

std::cout: writing output

cout_formatting.cpp
#include <iostream>
#include <iomanip>

int main() {
    std::cout << "Hello, C++!" << std::endl;

    // Chaining several values in one statement
    int age = 25;
    double height = 1.75;
    std::cout << "Age: " << age << ", Height: " << height << std::endl;

    // Number formatting
    std::cout << std::fixed << std::setprecision(2) << height << std::endl;

    // Hexadecimal and octal
    std::cout << std::hex << 255 << std::endl;      // ff
    std::cout << std::oct << 255 << std::endl;      // 377
    std::cout << std::dec << 255 << std::endl;      // 255

    return 0;
}

std::cin: reading input from the user

cin_input.cpp
#include <iostream>
#include <string>

int main() {
    // Read an integer
    int x;
    std::cout << "Enter an integer: ";
    std::cin >> x;

    // Read one word (stops at whitespace)
    std::string word;
    std::cout << "Enter a word: ";
    std::cin >> word;

    // Read a whole line, spaces included
    std::string line;
    std::cout << "Enter a sentence: ";
    std::getline(std::cin, line);

    std::cout << "You entered: " << line << std::endl;

    return 0;
}

5. Type casting: safe conversions and unsafe ones

Casting converts a value from one type to another. C++ gives you two families: the C-style cast (dangerous) and the C++ casts (safer).

C-style cast (not recommended)

Syntax: (type) value. Powerful, and dangerous precisely because the compiler does not check whether the conversion makes sense:

c_style_cast.cpp
int main() {
    double d = 3.14159;
    int i = (int)d;  // Truncates to 3 - data lost, and nothing warns you

    // Dangerous: reinterprets a string literal's address as an int*
    int* ptr = (int*) "dangerous string cast";  // UB!

    return 0;
}

static_cast: converting between compatible types

Syntax: static_cast<type>(value). Use it when you know the two types are compatible:

static_cast_demo.cpp
int main() {
    double d = 3.14159;
    int i = static_cast<int>(d);  // Explicit conversion, loss of precision OK

    // Between numeric types
    float f = static_cast<float>(42);

    // Up a class hierarchy: Derived* -> Base* is always safe
    class Base {};
    class Derived : public Base {};

    Derived d_obj;
    Base* base_ptr = static_cast<Base*>(&d_obj);  // Derived -> Base (safe)

    return 0;
}

const_cast: removing the const qualifier

Syntax: const_cast<type>(value). For when the data is not really const but was declared that way:

const_cast_demo.cpp
void modifyData(int* ptr) {
    *ptr = 999;
}

int main() {
    const int x = 10;

    // const_cast strips the const qualifier
    modifyData(const_cast<int*>(&x));

    // UNDEFINED BEHAVIOUR - and not only "if x sits in read-only memory".
    // Writing to an object that was DECLARED const is UB, full stop.
    // See what actually happens, measured, right below this block.

    return 0;
}

This article used to say "it is only a problem if x sits in read-only memory". That is wrong: writing to an object that was declared const is undefined behaviour wherever it lives. And it does not crash — it lies. Add a few prints and run it:

What it actually prints — one variable, two values
$ g++ -std=c++17 -O2 constcast.cpp && ./a.out
x doc truc tiep    = 10
x doc qua con tro  = 999
x doc qua p        = 999

The same variable x, in the same run, reads as 10 directly and 999 through a pointer. The compiler saw that x is const and folded the literal 10 into the direct read — a perfectly legal optimisation, since by the standard x cannot change. Memory really does hold 999. The program now carries two contradictory truths at once, with no warning at either -O0 or -O2.
const_cast is only safe when the underlying object is not const — an old API that takes char* while you are holding a const char* into a buffer you allocated yourself, for example.

reinterpret_cast: turning any pointer into any other pointer

Syntax: reinterpret_cast<type>(value). The most dangerous cast; reach for it only when nothing else will do (talking to an old API, or to hardware):

reinterpret_cast_demo.cpp
#include <iostream>

int main() {
    int x = 42;

    // Turn an address into an integer
    unsigned long long addr = reinterpret_cast<unsigned long long>(&x);

    std::cout << "Address: 0x" << std::hex << addr << std::endl;

    // ...and back again
    int* ptr = reinterpret_cast<int*>(addr);

    // Only reach for this when nothing else can express what you need

    return 0;
}

dynamic_cast: type checking at run time

All four above are resolved at compile time. dynamic_cast is different: it checks at run time whether the object really is the type you think, and returns nullptr when it is not. It only works on polymorphic classes (ones with virtual functions) — virtual and the VTable are Lesson 6's subject; here you only need to see how it differs from static_cast:

dynamic_cast_demo.cpp
#include <iostream>

struct Shape {
    virtual ~Shape() = default;   // Needs a virtual function, or dynamic_cast will not compile
};
struct Circle : Shape {
    void area() { std::cout << "  Circle::area\n"; }
};
struct Square : Shape {};

void handle(Shape* shape) {
    // Ask directly: is this object actually a Circle?
    if (Circle* c = dynamic_cast<Circle*>(shape)) {
        std::cout << "yes, it is a Circle\n";
        c->area();
    } else {
        std::cout << "NOT a Circle -> dynamic_cast returned nullptr\n";
    }
}

int main() {
    Circle c;
    Square sq;
    handle(&c);
    handle(&sq);

    // static_cast checks NOTHING: it takes your word for it, and stays silent.
    Circle* wrong = static_cast<Circle*>(static_cast<Shape*>(&sq));
    std::cout << "static_cast gave a non-null pointer? " << (wrong != nullptr) << "\n";

    return 0;
}
What it actually prints
yes, it is a Circle
  Circle::area
NOT a Circle -> dynamic_cast returned nullptr
static_cast gave a non-null pointer? 1

The last line is the one to remember. A Square is not a Circle, yet static_cast still handed back a non-null pointer — it checked nothing. Calling a Circle method through that pointer is undefined behaviour. That is the price of "faster": dynamic_cast costs a run-time table lookup, and in exchange it tells you the truth.

Type casting at a glance

Cast What it is for Safety
(type) value The old C-style cast Low — avoid
static_cast<T>() Compatible types (int↔float, Derived→Base) High
const_cast<T>() Stripping a const/volatile qualifier Medium
reinterpret_cast<T>() Any pointer to any other pointer Low — only when necessary
dynamic_cast<T>() Run-time type checking for polymorphic types High
📝 Check your understanding — Lesson 2
When should you use const auto& rather than auto in a range-based for loop?

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 cpp_fundamentals.cpp