This was migrated from version 1.0 and polished and expanded by Gemini.

Modern C++ Overview: From Low-Level Logic to Syntax Sugar

Preface: Encountered Modern C++ in a programming lab and could not defeat it with pure effort? After getting used to Java’s automatic memory management and Python’s flexibility, C++ syntax rules do look complicated, even a little strange.

Stereotypes about C++:

  • Old, out-dated, less-frequently used
  • Unsafe (the biggest pain point: memory leaks and out-of-bounds access)
  • Hard to use & Various Compilation Issues

Even so, we still need to learn Modern C++, because it remains irreplaceable in systems programming and high-performance computing.


1. Value Types & Move Semantics

lvalue vs rvalue

  • lvalue: An object that occupies an identifiable location in memory, namely an address. You can take its address with &.
  • rvalue: Usually a temporary object. You cannot take its address with &.

Copy Assignment vs Move Assignment

For an assignment operation a = xxx:

  1. Copy Assignment: Called when xxx is an lvalue.
  2. Move Assignment: Called when xxx is an rvalue.

std::move

A common rule of thumb: Move is faster than copy. What if we want to move from an lvalue?

We can use std::move. It makes the compiler treat an lvalue as an rvalue, enabling ownership transfer.

Note: After using std::move, the moved-from object should not be used in a meaningful way. Also, const variables cannot use move semantics effectively.

Common Cases Where std::move Should Not Be Used

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Print(const std::string &s);
std::string Concat(const std::string &p, std::string q) {
// 1. Move from a const var: const cannot be moved, so this still copies.
std::string tmp = std::move(p);

// 2. Move from an rvalue: redundant, because p + q is already an rvalue.
std::string tmp2 = std::move(p + q);

// 3. Move to a const reference: meaningless, because Print receives a reference.
Print(std::move(p + q));

// 4. Move the return value: wrong, because it can block RVO.
return std::move(ret);
}

2. Type Inference & std::forward

Universal Reference

In templates, T&& does not always mean an rvalue reference. It can be a universal reference, accepting both lvalues and rvalues.

1
2
3
4
5
6
7
8
template<typename T>
void func(T &&param) {
if constexpr (std::is_lvalue_reference_v<T>) {
std::cout << "The argument is an lvalue" << std::endl;
} else {
std::cout << "The argument is an rvalue" << std::endl;
}
}

3. Smart Pointers

Modern C++ uses smart pointers to manage memory and follows RAII, helping us move away from raw pointers.

std::unique_ptr

Core property: exclusive ownership. It cannot be copied, only moved.

Usage: create it with std::make_unique<T>(...).

std::shared_ptr

Core property: multiple owners can share the same resource.

Reference counting: a control block records how many pointers currently point to the object. When the count reaches zero, the object is automatically freed.

std::weak_ptr

This solves circular references. If A points to B and B also points to A, both as shared_ptr, they form a cycle and leak. weak_ptr does not increase the reference count. It can observe an object without extending its lifetime.

1
2
3
4
struct Node {
std::shared_ptr<Node> next; // If two Nodes point to each other, memory leaks.
// Solution: change one side to std::weak_ptr<Node> next;
};

4. Syntax Sugar

auto Inference

auto automatically deduces variable types, making code cleaner, especially with iterators or complex templates.

1
2
3
4
std::vector<int> vec = {1, 2, 3};
for (auto& elem : vec) { // auto& deduces a reference, so the container can be modified.
elem *= 2;
}

Structured Bindings - C++17

Extract multiple variables from std::pair or std::tuple at once.

1
2
3
4
std::map<std::string, int> scores = {{"Alice", 90}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << std::endl;
}

5. Safety: C++17 Safe Containers

These help avoid unsafe union usage or error-prone null pointers.

std::any

Stores a value of any type, but access must go through any_cast for type-safe checking.

std::optional

Represents a value that may be empty. You no longer need to return -1 or nullptr to express “invalid”.

1
2
3
4
std::optional<int> getValue(bool success) {
if (success) return 42;
return std::nullopt;
}

std::variant

A type-safe union. It knows which type it currently stores and can be handled elegantly with std::visit.

1
2
3
using MyType = std::variant<int, std::string>;
MyType v = "Modern C++";
std::visit([](auto&& arg) { std::cout << arg; }, v);