C++ is a powerful, general-purpose programming language. This document explores various C++ concepts, from fundamental syntax to advanced features introduced in modern C++ standards.
Core C++ Concepts
Arguments and Parameters
In C++, arguments are the actual values passed to a function during its invocation, while parameters are the variables declared in the function's signature that receive these arguments.
Default Arguments
Default argument values can only be specified in the function declaration, not in its definition.
Multithreading
C++ supports multithreading, allowing concurrent execution of multiple parts of a program. This involves managing threads, synchronization primitives, and potential race conditions.
Smart Pointers
Smart pointers (like std::shared_ptr, std::unique_ptr, and std::weak_ptr) automate memory management by using RAII (Resource Acquisition Is Initialization) principles to ensure resources are properly released, preventing memory leaks and dangling pointers.
Delegate Binding (Proxy Binding)
This concept relates to using objects as intermediaries or proxies to manage access to other objects or resources.
Design Patterns
Familiarity with common design patterns is crucial for writing maintainable and scalable C++ code.
STL Container Enhancements
emplace_back vs. push_back
emplace_back offers performance improvements over push_back by constructing elements directly in place within the container, eliminating the need for temporary object creation and subsequent copy/move operations.
emplace_back can accept constructor arguments directly, constructing the object in the container's memory:
#include <vector>
#include <string>
struct MyClass {
int id;
std::string name;
MyClass(int i, std::string s) : id(i), name(std::move(s)) {}
};
int main() {
std::vector<MyClass> vec;
// Using push_back requires constructing a temporary object first
// vec.push_back(MyClass(1, "hello")); // Temporary object + move operation
// Using emplace_back constructs the object directly in the vector's memory
vec.emplace_back(1, "hello"); // Direct construction, no temporary object needed
return 0;
}
emplace_back also supports implicit type conversions from arguments that can be used to construct the container's element type.
Move Semantics
Move semantics, enabled by rvalue references, allow for efficient transfer of resources from temporary objects to new objects, avoiding expensive deep copies.
When a function returns a temporary object, copy construction is invoked. However, the temporary object is immediately destructed after initialization. Move semantics optimize this by "moving" the resources instead of copying.
#include <iostream>
#include <string>
#include <utility> // For std::move
class CString {
public:
CString(const char* str = "") : data(new char[strlen(str) + 1]), length(strlen(str)) {
strcpy(data, str);
std::cout << "Constructor: " << data << std::endl;
}
CString(const CString& other) : length(other.length), data(new char[length + 1]) {
strcpy(data, other.data);
std::cout << "Copy Constructor: " << data << std::endl;
}
// Move constructor
CString(CString&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
std::cout << "Move Constructor: " << (data ? data : "null") << std::endl;
}
~CString() {
std::cout << "Destructor: " << (data ? data : "null") << std::endl;
delete[] data;
}
const char* c_str() const { return data; }
private:
char* data;
size_t length;
};
CString getString(const CString& str) {
const char* pstr = str.c_str();
CString tmpstr(pstr); // Normal constructor
return tmpstr; // RVO/NRVO might apply, otherwise move constructor
}
int main() {
CString str1("abcde");
CString str2;
str2 = getString(str1); // Move assignment or copy assignment
return 0;
}
Type Deduction and Declarations
typename Keyword
The typename keyword is used to explicitly inform the compiler that a dependent name within a template refers to a type.
Polymorphism in C++
Compile-Time Polymorphism
Compile-time polymorphism, also known as static polymorphism, is resolved during compilation. It is primarily achieved through function overloading and templates.
Run-Time Polymorphism
Run-time polymorphism, or dynamic polymorphism, is determined during program execution. It is typically implemented using virtual functions and pointers or references.
Virtual Functions and Virtual Function Tables (VTables)
When a function is declared as virtual in a base class, derived classes can override it. When called through a base class pointer or reference, the program dynamically determines the actual object type at runtime to invoke the correct version of the virtual function.
A VTable is an array of function pointers used to implement runtime polymorphism. Each class with virtual functions has a VTable containing addresses of its virtual functions. Objects of such classes store a pointer (vptr) to their class's VTable, enabling dynamic dispatch.
Multiple Inheritance and VTables
In multiple inheritance, an object may contain multiple VPtrs, each pointing to the VTable of a distinct base class.
Virtual Inheritance and VTables
Virtual inheritance resolves the "diamond problem" in multiple inheritance, ensuring a single instance of a shared base class. It introduces a virtual base pointer (vbptr) to track offsets to virtual base classes, with shared members often placed at the end of the object.
Function Pointers
Function names can often implicitly convert to pointers to functions. Explicitly taking the address using the & operator is also valid.
#include <iostream>
int print(int val) {
std::cout << "Value: " << val << std::endl;
return val;
}
int main() {
// Implicit conversion
int (*func_ptr)(int) = print;
func_ptr(10);
// Explicit address-of operator
int (*func_ptr_explicit)(int) = &print;
func_ptr_explicit(20);
return 0;
}
C++11 Features
NULL vs. nullptr
In C++, NULL is often defined as 0. C++11 introduced nullptr, a distinct pointer literal that provides better type safety than NULL.
Raw String Literals
Raw string literals, denoted by R"(...)", allow strings to be defined without escaping special characters.
constexpr
constexpr is used to declare constants and functions whose values can be computed at compile time. This enhances efficiency by performing computations during compilation rather than at runtime. Functions declared with constexpr must have a simple structure, and their initializations must be in the initializer list for constructors.
auto Type Deduction
The auto keyword allows the compiler to deduce the type of a variable from its initializer. Initialization is mandatory when using auto. auto typically ignores const and volatile qualifiers unless explicitly combined with them (e.g., const auto&).
decltype for Type Deduction
decltype(expression) deduces the type of an expression at compile time. It preserves reference and cv-qualifier information. It can be used for delayed type computation, especially in template metaprogramming and for specifying function return types using trailing return types.
#include <iostream>
#include <vector>
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
int main() {
int x = 5;
const int& cref = x;
decltype(cref) z = x; // z has type const int&
std::cout << "Type of z: " << typeid(z).name() << std::endl;
auto result = add(3, 4.5); // result type deduced by decltype in trailing return type
std::cout << "Result of add: " << result << std::endl;
return 0;
}
auto vs. decltype
- Deduction Rules:
autooften strips references and cv-qualifiers, whiledecltypepreserves them. - Usage:
autosimplifies declarations, whiledecltypeprecisely captures expression types for advanced scenarios.
volatile Keyword
The volatile keyword instructs the compiler not to perform certain optimizations on a variable. It ensures that accesses to the variable always read from and write to memory directly, guaranteeing visibility in environments where the variable's value might change unexpectedly (e.g., hardware registers, multithreaded scenarios).
Lvalues, Rvalues, and References
Lvalues and Rvalues
Lvalues represent objects with a persistent memory location and can appear on the left side of an assignment. They typically have names and can have their addresses taken.
Rvalues are temporary values that typically cannot appear on the left side of an assignment and usually do not have a persistent storage location. Examples include literals and temporary results of expressions.
Rvalue References
Rvalue references (&&) bind to rvalues. They are crucial for implementing move semantics and perfect forwarding.
#include <iostream>
#include <utility> // For std::move, std::forward
void processValue(int& val) {
std::cout << "Lvalue reference: " << val << std::endl;
}
void processValue(int&& val) {
std::cout << "Rvalue reference: " << val << std::endl;
// std::move(val) can be used here if ownership transfer is needed
}
template<typename T>
void forwarder(T&& arg) {
processValue(std::forward<T>(arg)); // Perfect forwarding
}
int main() {
int x = 10;
forwarder(x); // x is an lvalue, gets forwarded as an lvalue
forwarder(20); // 20 is an rvalue, gets forwarded as an rvalue
return 0;
}
Rvalue references enable:
- Move Semantics: Efficient resource transfer from temporary objects.
- Perfect Forwarding: Preserving the value category (lvalue/rvalue) of arguments when passing them through functions, using
std::forward.
final and override Specifiers
final can be applied to a virtual function to prevent further overriding in derived classes or to a class to prevent it from being inherited. override, when applied to a member function, asserts that the function is intended to override a base class virtual function, providing compile-time checking.
Constructor Enhancements
Delegating Constructors
Delegating constructors allow one constructor to call another constructor within the same class, reducing code duplication. The call must occur in the constructor's initializer list.
class MyClass {
int value1;
int value2;
public:
// Constructor 1
MyClass(int v1) : value1(v1), value2(0) {}
// Constructor 2 delegating to Constructor 1
MyClass(int v1, int v2) : MyClass(v1) { // Calls MyClass(int v1)
value2 = v2;
}
};
Inheriting Constructors
In C++17, derived classes can inherit constructors from base classes using the using declaration.
class Base {
public:
Base(int val) : data(val) {}
int data;
};
class Derived : public Base {
public:
using Base::Base; // Inherits Base constructors
};
int main() {
Derived d(10); // Calls Base::Base(10)
// std::cout << d.data << std::endl; // Output: 10
return 0;
}
Initializer Lists
std::initializer_list<T> provides a convenient way to pass a variable number of elements of the same type to functions or constructors.
Range-Based For Loops
Range-based for loops simplify iteration over containers, arrays, and initializer lists.
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}};
for (const auto& pair : myMap) {
// Note: The key in std::map is const even without explicit const
std::cout << "{" << pair.first << ": " << pair.second << "} ";
}
std::cout << std::endl;
return 0;
}
Keys in std::map and std::set are inherently read-only, evenif not explicitly declared const in the loop variable.
Lambda Expressions
Lambda expressions provide a concise way to define anonymous functions, often used with algorithms or for creating callbacks. They are typically treated as function objects (functors) by the compiler.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9};
// Basic lambda
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b; // Sort in ascending order
});
std::cout << "Sorted: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
int multiplier = 3;
// Lambda with capture by value
auto multiply_by_3 = [multiplier](int x) {
return x * multiplier;
};
std::cout << "10 * multiplier = " << multiply_by_3(10) << std::endl;
// Lambda with capture by reference
int factor = 2;
auto add_factor = [&factor](int x) {
factor++; // Modify captured variable
return x + factor;
};
std::cout << "5 + factor = " << add_factor(5) << std::endl; // factor becomes 3
std::cout << "10 + factor = " << add_factor(10) << std::endl; // factor becomes 4
return 0;
}
The syntax is [capture](parameters) mutable exception attribute -> return_type { body };
- Capture Clause:
[]: No capture.[&]: Capture all by reference.[=]: Capture all by value (read-only inside lambda unlessmutable).[=, &var]: Capture all by value,varby reference.[&, var]: Capture all by reference,varby value.[this]: Capture thethispointer.
Functors (Function Objects)
A functor is a class or struct that overloads the function call operator (operator()). This allows objects of that class to be called like functions.
#include <iostream>
class Multiplier {
private:
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
int main() {
Multiplier times_3(3);
std::cout << "10 * 3 = " << times_3(10) << std::endl; // Output: 30
return 0;
}
using Declaration
The using keyword has several uses:
- Creating aliases for types (similar to
typedef, but more flexible, especially with templates). - Bringing names from a namespace into the current scope.
- In derived classes, inheriting base class members, including constructors.
#include <iostream>
#include <vector>
#include <map>
// Type alias using using
using uint_t = unsigned int;
using IntMap = std::map<int, int>;
// Template alias (requires a wrapper struct or class pre-C++11)
template <typename T>
using VecOf = std::vector<T>;
int main() {
uint_t counter = 100;
IntMap scores;
VecOf<std::string> names;
std::cout << "Counter: " << counter << std::endl;
return 0;
}
Callable Objects
Callable objects in C++ include function pointers, objects with overloaded operator() (functors), objects convertible to function pointers, and pointers to class members.
Callable Wrapper: std::function
std::function is a general-purpose polymorphic function wrapper. It can store, copy, and invoke any callable target—functions, lambda expressions, bind expressions, or function objects—that matches a specific function signature.
#include <iostream>
#include <functional> // For std::function
#include <string>
void printMessage(int id, const std::string& msg) {
std::cout << "ID: " << id << ", Message: " << msg << std::endl;
}
class MyCallable {
public:
void operator()(int id, const std::string& msg) const {
std::cout << "Callable Object - ID: " << id << ", Message: " << msg << std::endl;
}
};
int main() {
// Wrap a regular function
std::function<void(int, std::string)> func1 = printMessage;
func1(1, "Hello from function pointer");
// Wrap a functor
MyCallable callableObj;
std::function<void(int, std::string)> func2 = callableObj;
func2(2, "Hello from functor");
// Wrap a lambda
std::function<void(int, std::string)> func3 = [](int id, const std::string& msg) {
std::cout << "Lambda - ID: " << id << ", Message: " << msg << std::endl;
};
func3(3, "Hello from lambda");
return 0;
}
Binder: std::bind
std::bind creates a function object from a callable target and a set of arguments. It can be used to:
- Bind arguments to a function, creating a new callable object with fewer arguments.
- Reorder arguments or use placeholders (
std::placeholders::_1,_2, etc.).
#include <iostream>
#include <functional> // For std::bind, std::placeholders
void output(int x, int y) {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
int main() {
// Bind arguments: output(10, 20) will be called
auto bound_func1 = std::bind(output, 10, 20);
bound_func1(); // No arguments needed as all are bound
// Bind first argument, use placeholder for the second
auto bound_func2 = std::bind(output, 10, std::placeholders::_1);
bound_func2(30); // 30 is passed to std::placeholders::_1
// Bind second argument, use placeholder for the first
auto bound_func3 = std::bind(output, std::placeholders::_1, 20);
bound_func3(40); // 40 is passed to std::placeholders::_1
// Reorder arguments
auto bound_func4 = std::bind(output, std::placeholders::_2, std::placeholders::_1);
bound_func4(50, 60); // 50 goes to _1, 60 goes to _2. Output: x: 60, y: 50
return 0;
}
Rvalue References and Move Semantics
Rvalue references (T&&) are key to move semantics. They allow resources to be efficiently transferred from expiring temporary objects (rvalues) rather than being copied.
#include <iostream>
#include <utility> // For std::move
#include <list>
#include <string>
class Resource {
public:
int* data;
Resource(int size) : data(new int[size]) {
std::cout << "Resource acquired." << std::endl;
}
// Copy constructor (deep copy)
Resource(const Resource& other) : data(new int[10]) { // Assume size 10 for simplicity
if (other.data) {
// Copy data
}
std::cout << "Resource copied." << std::endl;
}
// Move constructor
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr; // Nullify the moved-from object's pointer
std::cout << "Resource moved." << std::endl;
}
~Resource() {
std::cout << "Resource released." << std::endl;
delete[] data;
}
};
Resource createResource() {
Resource res(10);
return res; // Return Value Optimization (RVO) or move construction
}
int main() {
Resource r1(5);
// Resource r2 = r1; // Calls copy constructor
Resource r3 = std::move(r1); // Calls move constructor, r1's resource is now null
Resource r4 = createResource(); // Uses move construction if RVO is not applied
std::list<std::string> ls1{"hello", "world"};
std::list<std::string> ls2 = std::move(ls1); // Efficiently transfers resources from ls1 to ls2
return 0;
}
std::move casts its argument to an rvalue reference, enabling move semantics. std::forward is used in template functions to preserve the original value category of arguments when passing them recursively.
Smart Pointers
Smart pointers manage dynamic memory automatically.
std::shared_ptr
shared_ptr uses reference counting. Multiple shared_ptr instances can point to the same object, and the object is deleted only when the last shared_ptr pointing to it goes out of scope or is reset.
#include <iostream>
#include <memory> // For smart pointers
#include <vector>
struct MyData {
int value;
MyData(int v) : value(v) { std::cout << "MyData(" << value << ") constructed." << std::endl; }
~MyData() { std::cout << "MyData(" << value << ") destructed." << std::endl; }
};
int main() {
// Initialization using new
std::shared_ptr<MyData> ptr1(new MyData(10));
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // Output: 1
// Copy construction increases use_count
std::shared_ptr<MyData> ptr2 = ptr1;
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // Output: 2
std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // Output: 2
// Move construction does not increase use_count
std::shared_ptr<MyData> ptr3 = std::move(ptr1);
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // Output: 0 (ptr1 is now empty)
std::cout << "ptr3 use_count: " << ptr3.use_count() << std::endl; // Output: 2
// Initialization using make_shared (preferred)
auto ptr4 = std::make_shared<MyData>(20);
std::cout << "ptr4 use_count: " << ptr4.use_count() << std::endl; // Output: 1
// Using reset()
ptr2.reset(); // Decreases use_count for the object ptr1/ptr2/ptr3 pointed to
std::cout << "ptr2 after reset use_count: " << ptr2.use_count() << std::endl; // Output: 0
std::cout << "ptr3 use_count after ptr2 reset: " << ptr3.use_count() << std::endl; // Output: 1
ptr3.reset(new MyData(30)); // ptr3 now manages a new object
std::cout << "ptr3 use_count after reset with new: " << ptr3.use_count() << std::endl; // Output: 1
return 0;
} // ptr4 and ptr3 go out of scope, decrementing counts and potentially destructing objects
Custom deleters can be provided to shared_ptr for specialized cleanup tasks, especially for arrays or non-standard memory management.
std::unique_ptr
unique_ptr provides exclusive ownership of a dynamically allocated object. Only one unique_ptr can own the object at a time. Ownership can be transferred using std::move.
#include <iostream>
#include <memory> // For unique_ptr
#include <utility> // For std::move
struct Widget {
int id;
Widget(int i) : id(i) { std::cout << "Widget(" << id << ") created." << std::endl; }
~Widget() { std::cout << "Widget(" << id << ") destroyed." << std::endl; }
};
int main() {
// Initialization
std::unique_ptr<Widget> u_ptr1(new Widget(1));
std::cout << "u_ptr1 ID: " << u_ptr1->id << std::endl;
// Transfer ownership
std::unique_ptr<Widget> u_ptr2 = std::move(u_ptr1);
// u_ptr1 is now null
if (!u_ptr1) {
std::cout << "u_ptr1 is now null." << std::endl;
}
std::cout << "u_ptr2 ID: " << u_ptr2->id << std::endl;
// Using reset()
u_ptr2.reset(new Widget(2)); // Old widget is deleted, new one managed
std::cout << "u_ptr2 ID after reset: " << u_ptr2->id << std::endl;
// unique_ptr can manage arrays
std::unique_ptr<Widget[]> u_array_ptr(new Widget[3]{Widget(3), Widget(4), Widget(5)});
std::cout << "Array element 0 ID: " << u_array_ptr[0].id << std::endl;
return 0;
} // u_array_ptr and u_ptr2 go out of scope, managing their respective resources
std::weak_ptr
weak_ptr provides a non-owning "weak" reference to an object managed by shared_ptr. It's used to break circular references and to observe objects without affecting their lifetime.
- It does not increment the reference count.
- Use
lock()to obtain ashared_ptr(if the object still exists). - Use
expired()to check if the managed object has been deleted.
Storage Durations and Memory Layout
C++ memory is broadly divided into segments:
- Stack: For local variables, function parameters, and return addresses. Managed automatically by the compiler.
- Heap: For dynamically allocated memory (
new,malloc). Requires manual deallocation or management by smart pointers. - Global/Static Storage: For global variables, static variables, and string literals. Allocated at program start, deallocated at program end.
- Code Segment: Stores executable instructions.
- Constant Memory: Stores read-only data.
const Qualifier
The const qualifier indicates that a variable or object cannot be modified after initialization.
const int* ptr;orint const* ptr;: Pointer to a constant integer (the integer cannot be changed viaptr).int* const ptr;: Constant pointer (the pointer itself cannot be changed to point elsewhere).const int* const ptr;: Constant pointer to a constant integer.
const member functions guarantee that they do not modify the object's state and can be called on both const and non-const objects. Non-const objects can call both const and non-const member functions, while const objects can only call const member functions.
static Keyword
The static keyword has different meanings depending on its context:
- File Scope: Limits visibility of variables/functions to the current translation unit (prevents redefinition errors).
- Function Scope (Local Static): Creates variables with static storage duration that are initialized only once and retain their value across function calls, while remaining local in scope.
- Class Scope:
- Static Data Members: Shared among all objects of the class.
- Static Member Functions: Belong to the class, not an object; can be called using the class name (e.g.,
ClassName::staticMethod()) and can only access static members.
explicit Keyword
The explicit keyword prevents a constructor from being used for implicit type conversions. It forces the programmer to perform explicit conversions.
#include <iostream>
#include <string>
class MyString {
public:
// explicit prevents implicit conversion from const char*
explicit MyString(const char* s) : str(s) {
std::cout << "MyString created with: " << str << std::endl;
}
// For demonstration, assume operator<< is overloaded elsewhere or use a method
void print() const { std::cout << str; }
private:
const char* str;
};
void displayString(const MyString& s) {
s.print();
std::cout << std::endl;
}
int main() {
// displayString("Hello"); // Error: implicit conversion from const char* is disallowed
// Explicit conversion is required
displayString(MyString("Hello"));
displayString(static_cast<MyString>("World"));
return 0;
}
References
A reference is an alias for an existing object. It must be initialized upon declaration and cannot be changed to refer to a different object later.
#include <iostream>
int main() {
int original = 10;
int& ref = original; // ref is an alias for original
ref = 20; // Modifies original through ref
std::cout << "Original: " << original << ", Ref: " << ref << std::endl; // Output: Original: 20, Ref: 20
// Cannot reference a literal directly without const
// int& bad_ref = 10; // Error
// A const reference can bind to temporaries/literals
const int& const_ref = 10;
std::cout << "Const Ref: " << const_ref << std::endl; // Output: Const Ref: 10
return 0;
}
Returning references from functions requires care, especially when returning references to local variables (which leads to dangling references). Returning references to static variables is safe as they persist throughout the program's lifetime.
Copy Constructors and Deep/Shallow Copy
A copy constructor is called when a new object is created as a copy of an existing object (e.g., pass-by-value, return-by-value, initialization).
- Shallow Copy: The default copy constructor performs a member-wise copy. If members are pointers to dynamically allocated memory, this results in multiple objects pointing to the same memory, leading to double deletion issues.
- Deep Copy: A user-defined copy constructor explicitly allocates new memory and copies the data, ensuring each object has its own independent resources.
Inline Functions vs. Macros
Inline functions suggest to the compiler that the function body should be expanded at the call site, potentially reducing function call overhead. However, excessive inlining can lead to code bloat.
Macros are preprocessor text replacements and lack type checking, making them error-prone. Inline functions offer type safety and are generally preferred over macros for performance optimizations.
Class Member Initialization Order
When a class contains members that are objects of other classes, the member objects are constructed before the constructor of the containing class body executes.
friend Declarations
The friend keyword allows a function or another class to access the private and protected members of a class, breaking encapsulation for specific, controlled access.
Templates
Templates enable generic programming, allowing code to operate on types not yet specified. The compiler instantiates template code for specific types upon usage.
- Template Specialization: Allows providing custom implementations for specific types.
- Non-Type Template Parameters: Allow using integral constants or pointers as template arguments.
#include <iostream>
#include <cstring> // For strcmp
// Generic template
template<typename T>
bool compare(T a, T b) {
std::cout << "Generic compare called." << std::endl;
return a > b;
}
// Explicit specialization for const char*
template<>
bool compare<const char*>(const char* a, const char* b) {
std::cout << "Specialized compare for const char* called." << std::endl;
return std::strcmp(a, b) > 0;
}
// Non-type template parameter example
template<typename T, int Size>
void printArray(T (&arr)[Size]) {
std::cout << "Array of size " << Size << ": ";
for (int i = 0; i < Size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
int main() {
compare(10, 5); // Calls generic compare<int>
compare(10.5, 5.2); // Calls generic compare<double>
const char* str1 = "apple";
const char* str2 = "banana";
compare(str1, str2); // Calls specialized compare<const char="">
int numbers[] = {1, 2, 3, 4};
printArray(numbers); // Size is deduced as 4
return 0;
}
</const></double></int>
Allocator
An allocator in C++ separates memory allocation/deallocation from object construction/destruction. This allows for fine-grained control over memory management, such as using memory pools or custom allocation strategies.
Multithreading
C++11 provides threading support via the <thread> and <mutex> headers.
std::thread: Create and manage threads.join(): Wait for a thread to complete.detach(): Allow a thread to run independently.std::mutexandstd::lock\_guard/std::unique\_lock: For synchronization and protecting critical sections.std::atomicand CAS (Compare-And-Swap): For lock-free operations on primitive types.thread\_local: Declares variables that have separate instances per thread.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex cout_mutex; // Mutex to protect std::cout
void worker_function(int id) {
{
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "Thread " << id << " started." << std::endl;
}
// Simulate work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
{
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "Thread " << id << " finished." << std::endl;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker_function, i); // Create and start threads
}
for (auto& t : threads) {
t.join(); // Wait for all threads to complete
}
std::cout << "All threads finished." << std::endl;
return 0;
}
STL Container Adapters
Container adapters provide a specific interface on top of underlying containers.
std::stack
std::stack is a Last-In, First-Out (LIFO) container adapter. It does not provide iterators as it restricts access to the top element. It can be implemented using std::deque (default), std::vector, or std::list.
#include <stack>
#include <vector>
#include <iostream>
int main() {
// Stack with default underlying container (deque)
std::stack<int> s1;
s1.push(10);
s1.push(20);
std::cout << "Top of s1: " << s1.top() << std::endl; // Output: 20
s1.pop();
// Stack with std::vector as underlying container
std::stack<int, std::vector<int> > s2;
s2.push(30);
s2.push(40);
std::cout << "Top of s2: " << s2.top() << std::endl; // Output: 40
return 0;
}
std::vector
std::vector is a dynamic array that can resize itself automatically.
Initialization
- Default constructor: Creates an empty vector.
- Size initialization:
std::vector<int> v(5, 100);(5 elements, each initialized to 100). - Range initialization: From arrays, other vectors, or iterators.
assign()method: Replaces vector contents with new elements.
Releasing Memory
To explicitly release memory held by a vector:
vector.clear(); vector.shrink_to_fit();(C++11 onwards)std::vector<T>().swap(vec);orvec.swap(std::vector<T>());
Data Copying Utilities
- STL Algorithms:
std::copy(src.begin(), src.end(), dest.begin());andstd::copy_n(). - C-style:
memcpy(destination, source, num_bytes);