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:
#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.
namespace stdnamespace 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:
#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 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:
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 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 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:
#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
autowhen 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:
#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;
}
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 anint.
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
#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
#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:
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:
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:
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:
$ 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):
#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:
#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;
}
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 |
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
Comments