Introduction to Modern C++ (C++11/14/17/20)
This guide documents key features from the Modern C++ Tutorial: Quick Start with C++11/14/17/20.
Chapter 2: Core Language Enhancements
Compile-time Constants with constexpr
The constexpr keyword allows expressions to be evaluated at compile time, enabling optimizations.
constexpr int fibonacci_sequence(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci_sequence(n-1) + fibonacci_sequence(n-2);
}
Enhanced Conditional Statements
Variables declared within conditional statements can be used directly within their scopes.
// Declare iterator directly in if condition
if (const std::vector<int>::iterator position = std::find(data_collection.begin(), data_collection.end(), target_value);
position != data_collection.end()) {
*position = replacement_value;
}
Automatic Type Deduction with auto
The auto keyword automatically deduces variable types at declaration time (not applicable to function parameters).
auto numerical_value = 42; // Deduced as int
auto dynamic_array = new auto(15); // Deduced as int*
Type Inquiry with decltype
decltype determines the type of an expression or variable, providing type information without auto's initialization requirement.
auto first_number = 10;
auto second_number = 20;
decltype(first_number + second_number) result;
Trailing Return Type Syntax
Function return types can be specified after the parameter list, allowing for more complex type declarations.
template<typename Type1, typename Type2>
auto calculate_sum(Type1 x, Type2 y) -> decltype(x + y) {
return x + y;
}
Range-based For Loops
Simplified iteration over container elements, similar to other modern languages.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4};
if (auto iterator = std::find(numbers.begin(), numbers.end(), 3); iterator != numbers.end())
*iterator = 4;
// Read-only iteration
for (auto element : numbers)
std::cout << element << std::endl;
// Modifiable iteration
for (auto &element : numbers) {
element += 1;
}
// Read-only iteration after modification
for (auto element : numbers)
std::cout << element << std::endl;
}
Strongly-typed Enums
Enum classes prevent namespace pollution and provide type safety for enum values.
enum class color_code : unsigned int {
red,
green,
blue = 100,
yellow = 100
};
Chapter 3: Advanced Language Features
Lambda Expressions
Anonymous functions that can capture variables from their enclosing scope.
[capture_list](parameters) mutable(exception_specification) -> return_type {
// function body
}
The capture list allows passing external variables into the lambda. There are two capture methods:
Value Capture: Copies the variable into the lambda.
void demonstrate_value_capture() {
int value = 1;
auto capture_by_value = [value] {
return value;
};
value = 100;
auto stored_value = capture_by_value();
std::cout << "stored_value = " << stored_value << std::endl;
// At this point, stored_value == 1, while value == 100
// The lambda preserved a copy of the original value
}
Reference Capture: Creates a reference to the original variable.
void demonstrate_reference_capture() {
int value = 1;
auto capture_by_reference = [&value] {
return value;
};
value = 100;
auto stored_value = capture_by_reference();
std::cout << "stored_value = " << stored_value << std::endl;
// At this point, stored_value == 100, same as value
// The lambda references the original variable
}
Lvalue and Rvalue References
Lvalues (left values) are persistent objects that exist beyond a single expression. For example:
int x = 10 + 5; // x is an lvalue
Rvalues (right values) are temporary objects that cease to exist after an expression. For example:
std::vector<int> generate_data();
std::vector<int> data = generate_data(); // The return value is temporary
Prvalues (pure rvalues) are either literal values (like 10, true) or temporary objects (like 1+2).
Xvalues (expiring values) are objects that are about to be destroyed but can be moved, introduced in C++11 to support move semantics.
Example demonstrating reference types:
#include <iostream>
#include <string>
void process_reference(std::string& str) {
std::cout << "Lvalue reference" << std::endl;
}
void process_reference(std::string&& str) {
std::cout << "Rvalue reference" << std::endl;
}
int main()
{
std::string left_value = "Hello, ";
// std::string&& r1 = left_value; // Illegal: rvalue reference cannot bind to lvalue
std::string&& right_value = std::move(left_value); // Legal: std::move converts lvalue to rvalue
std::cout << right_value << std::endl; // Hello,
const std::string& extended_left_value = left_value + left_value; // Legal: const lvalue extends lifetime
// extended_left_value += " World"; // Illegal: const reference cannot be modified
std::string&& extended_right_value = left_value + extended_left_value; // Legal: rvalue extends lifetime
extended_right_value += " World"; // Legal: non-const reference can modify temporary
std::cout << extended_right_value << std::endl; // Hello, Hello, World
process_reference(extended_right_value); // Outputs Lvalue reference
return 0;
}
Move Semantics
Move semantics allow efficient transfer of resources from temporary objects rather than expensive copying.
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main() {
std::string original = "Resource-intensive data";
std::vector<std::string> container;
// Uses push_back(const T&), creating a copy
container.push_back(original);
std::cout << "original: " << original << std::endl;
// Uses push_back(T&&), moving resources without copying
// After this operation, original becomes empty
container.push_back(std::move(original));
std::cout << "original after move: " << original << std::endl;
return 0;
}
Chapter 4: Container Improvements
Fixed-size Arrays (std::array)
Unlike std::vector, std::array has a fixed size and doesn't automatically shrink when elements are removed.
std::array<int, 4> arr = {1, 2, 3, 4};
arr.empty(); // Check if container is empty
arr.size(); // Return number of elements
// Iterator support
for (auto &element : arr)
{
// Process element
}
// Sort with lambda expression
std::sort(arr.begin(), arr.end(), [](int a, int b) {
return b < a;
});
// Array size must be compile-time constant
constexpr int size = 4;
std::array<int, size> arr = {1, 2, 3, 4};
// Unlike C-style arrays, std::array doesn't decay to T*
// int *arr_ptr = arr; // Illegal
Unordered Containers
std::unordered_map and std::unordered_set use hash tables instead of binary trees, providing O(1) average complexity for insertions and lookups when element order isn't important.
#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
int main() {
// Initialize with same order
std::unordered_map<int, std::string> unordered_container = {
{1, "First"},
{3, "Third"},
{2, "Second"}
};
std::map<int, std::string> ordered_container = {
{1, "First"},
{3, "Third"},
{2, "Second"}
};
// Iterate through both containers
std::cout << "std::unordered_map" << std::endl;
for( const auto & entry : unordered_container)
std::cout << "Key:[" << entry.first << "] Value:[" << entry.second << "]\n";
std::cout << std::endl;
std::cout << "std::map" << std::endl;
for( const auto & entry : ordered_container)
std::cout << "Key:[" << entry.first << "] Value:[" << entry.second << "]\n";
}
Tuples
Tuples store elements of different types with utilities for creation, access, and unpacking.
#include <tuple>
#include <iostream>
auto create_person(int id)
{
// Return type inferred as std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "Alice");
if (id == 1)
return std::make_tuple(2.9, 'C', "Bob");
if (id == 2)
return std::make_tuple(1.7, 'D', "Charlie");
return std::make_tuple(0.0, 'D', "Unknown");
// Returning just 0 would cause inference error
}
int main()
{
auto person = create_person(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(person) << ", "
<< "Grade: " << std::get<1>(person) << ", "
<< "Name: " << std::get<2>(person) << '\n';
double gpa;
char grade;
std::string name;
// Tuple unpacking
std::tie(gpa, grade, name) = create_person(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "Grade: " << grade << ", "
<< "Name: " << name << '\n';
}
Chapter 5: Smart Pointers
Smart pointers (std::shared_ptr, std::unique_ptr, std::weak_ptr) are defined in the header.
std::shared_ptr
Shared pointers maintain reference counts and automatically delete objects when no references remain.
#include <iostream>
#include <memory>
void increment_value(std::shared_ptr<int> counter)
{
(*counter)++;
}
int main()
{
// auto pointer = new int(10); // Illegal: no direct assignment
// Create a std::shared_ptr
auto pointer = std::make_shared<int>(10);
increment_value(pointer);
std::cout << *pointer << std::endl; // 11
// The shared_ptr will be automatically destroyed when leaving scope
return 0;
}
Shared pointer management methods:
auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // Reference count increases
auto pointer3 = pointer; // Reference count increases
int *raw_ptr = pointer.get(); // Doesn't increase reference count
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "after reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "after reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0
std::unique_ptr
Unique pointers enforce exclusive ownership of an object and can be transferred using move semantics.
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique available since C++14
// std::unique_ptr<int> pointer2 = pointer; // Illegal: copy constructor deleted
#include <iostream>
#include <memory>
struct Data {
Data() { std::cout << "Data::Data" << std::endl; }
~Data() { std::cout << "Data::~Data" << std::endl; }
void process() { std::cout << "Data::process" << std::endl; }
};
void process_data(const Data &) {
std::cout << "process_data(const Data&)" << std::endl;
}
int main() {
std::unique_ptr<Data> p1(std::make_unique<Data>());
// p1 is not empty, output
if (p1) p1->process();
{
std::unique_ptr<Data> p2(std::move(p1));
// p2 is not empty, output
process_data(*p2);
// p2 is not empty, output
if(p2) p2->process();
// p1 is empty, no output
if(p1) p1->process();
p1 = std::move(p2);
// p2 is empty, no output
if(p2) p2->process();
std::cout << "p2 is destroyed" << std::endl;
}
// p1 is not empty, output
if (p1) p1->process();
// Data instance will be destroyed when leaving scope
}
Chapter 7: Concurrency and Parallelism
Threads
std::thread creates new threads, with join() waiting for thread completion.
#include <iostream>
#include <thread>
int main() {
std::thread worker([](){
std::cout << "Worker thread executing." << std::endl;
});
worker.join();
return 0;
}
Mutexes and Critical Sections
std::mutex provides basic mutual exclusion, but requires careful handling to avoid deadlocks. std::lock_guard offers RAII-style locking.
#include <iostream>
#include <thread>
int shared_value = 1;
void update_value(int new_value) {
static std::mutex value_mutex;
std::lock_guard<std::mutex> lock(value_mutex);
// Perform critical operation
shared_value = new_value;
// Mutex is automatically released when leaving scope
}
int main() {
std::thread t1(update_value, 2), t2(update_value, 3);
t1.join();
t2.join();
std::cout << shared_value << std::endl;
return 0;
}
std::unique_lock offers more flexibility with manual lock management.
#include <iostream>
#include <thread>
int shared_value = 1;
void update_value(int new_value) {
static std::mutex value_mutex;
std::unique_lock<std::mutex> lock(value_mutex);
// Perform critical operation
shared_value = new_value;
std::cout << shared_value << std::endl;
// Release lock manually
lock.unlock();
// Other threads can access shared_value during this period
// Acquire lock again for another critical section
lock.lock();
shared_value += 1;
std::cout << shared_value << std::endl;
}
int main() {
std::thread t1(update_value, 2), t2(update_value, 3);
t1.join();
t2.join();
return 0;
}
Condition Variables
Condition variables enable thread synchronization based on state changes, useful for producer-consumer patterns.
#include <queue>
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
#include <condition_variable>
int main() {
std::queue<int> data_queue;
std::mutex queue_mutex;
std::condition_variable data_condition;
bool notification_flag = false; // Notification state
// Producer thread
auto producer = [&]() {
for (int i = 0; ; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(900));
std::unique_lock<std::mutex> lock(queue_mutex);
std::cout << "Producing " << i << std::endl;
data_queue.push(i);
notification_flag = true;
data_condition.notify_all(); // Can also use notify_one
}
};
// Consumer thread
auto consumer = [&]() {
while (true) {
std::unique_lock<std::mutex> lock(queue_mutex);
while (!notification_flag) { // Prevent spurious wakeups
data_condition.wait(lock);
}
// Briefly unlock to allow producer to continue while consumer processes
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Consumer slower than producer
lock.lock();
while (!data_queue.empty()) {
std::cout << "Consuming " << data_queue.front() << std::endl;
data_queue.pop();
}
notification_flag = false;
}
};
// Run in separate threads
std::thread p(producer);
std::thread cs[2];
for (int i = 0; i < 2; ++i) {
cs[i] = std::thread(consumer);
}
p.join();
for (int i = 0; i < 2; ++i) {
cs[i].join();
}
return 0;
}
Conclusion
Modern C++ features significantly enhance code expressiveness and performance. The most important concepts to master are smart pointers and lambda expressions, which are prevalent in contemporary C++ codebases.