C++ offers several advanced features that enhance code flexibility, efficiency, and expressiveness. Let's explore templates, the Standard Template Library (STL), move semantics, and lambda expressions.
Templates are a powerful feature in C++ that allows you to write generic functions and classes that can work with different data types without having to rewrite the code for each type.
Function templates define a family of functions. You can use them to create functions that perform the same operation on different types of data.
#include <iostream>
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << "Max of 5 and 10: " << max(5, 10) << std::endl;
std::cout << "Max of 5.2 and 3.7: " << max(5.2, 3.7) << std::endl;
std::cout << "Max of 'a' and 'c': " << max('a', 'c') << std::endl;
std::cout << "Max of strings 'hello' and 'world': " << max(std::string("hello"), std::string("world")) << std::endl;
return 0;
}
Class templates define a family of classes. They are useful for creating generic classes like containers that can hold elements of any type.
#include <iostream>
#include <vector>
template <typename T>
class MyVector {
private:
std::vector<T> data;
public:
void push_back(const T& value) {
data.push_back(value);
}
T get(size_t index) const {
if (index < data.size()) {
return data[index];
}
throw std::out_of_range("Index out of bounds");
}
size_t size() const {
return data.size();
}
};
int main() {
MyVector<int> intVector;
intVector.push_back(10);
intVector.push_back(20);
std::cout << "Integer vector size: " << intVector.size() << ", Element at 0: " << intVector.get(0) << std::endl;
MyVector<std::string> stringVector;
stringVector.push_back("apple");
stringVector.push_back("banana");
std::cout << "String vector size: " << stringVector.size() << ", Element at 1: " << stringVector.get(1) << std::endl;
return 0;
}
Template specialization allows you to provide a specific implementation of a template for a particular data type. This is useful when the generic implementation doesn't work well or isn't efficient for a certain type.
#include <iostream>
#include <string>
template <typename T>
struct Printer {
void print(const T& value) {
std::cout << "Generic print: " << value << std::endl;
}
};
// Specialization for std::string
template <>
struct Printer<std::string> {
void print(const std::string& value) {
std::cout << "String print: \"" << value << "\"" << std::endl;
}
};
int main() {
Printer<int> intPrinter;
intPrinter.print(123);
Printer<std::string> stringPrinter;
stringPrinter.print("hello");
Printer<double> doublePrinter;
doublePrinter.print(3.14);
return 0;
}
The STL is a powerful set of template classes and functions that provide common programming data structures and algorithms. It consists of containers, algorithms, and iterators.
Containers are template classes that store collections of objects.
vector
: A dynamic array that can grow or shrink as needed.list
: A doubly-linked list that supports efficient insertion and deletion.map
: An associative container that stores key-value pairs, sorted by key.unordered_map
: An associative container that stores key-value pairs, allowing fast average-case lookup based on hash values of the keys.set
: A container that stores unique elements, sorted by value.unordered_set
: A container that stores unique elements, allowing fast average-case lookup based on hash values of the elements.#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
int main() {
std::vector<int> vec = {1, 2, 3};
std::cout << "Vector: "; for (int x : vec) std::cout << x << " "; std::cout << std::endl;
std::list<int> lst = {3, 2, 1};
std::cout << "List: "; for (int x : lst) std::cout << x << " "; std::cout << std::endl;
std::map<std::string, int> mp = {{"apple", 1}, {"banana", 2}};
std::cout << "Map: "; for (const auto& pair : mp) std::cout << pair.first << ":" << pair.second << " "; std::cout << std::endl;
std::unordered_map<std::string, int> ump = {{"apple", 1}, {"banana", 2}};
std::cout << "Unordered Map: "; for (const auto& pair : ump) std::cout << pair.first << ":" << pair.second << " "; std::cout << std::endl;
std::set<int> s = {3, 1, 2};
std::cout << "Set: "; for (int x : s) std::cout << x << " "; std::cout << std::endl;
std::unordered_set<int> us = {3, 1, 2};
std::cout << "Unordered Set: "; for (int x : us) std::cout << x << " "; std::cout << std::endl;
return 0;
}
The STL provides a rich set of generic algorithms that operate on ranges of elements defined by iterators. These algorithms can perform operations like sorting, searching, transforming, and more.
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // For std::greater
int main() {
std::vector<int> numbers = {5, 1, 4, 2, 8};
std::sort(numbers.begin(), numbers.end());
std::cout << "Sorted (ascending): "; for (int n : numbers) std::cout << n << " "; std::cout << std::endl;
std::sort(numbers.begin(), numbers.end(), std::greater<int>());
std::cout << "Sorted (descending): "; for (int n : numbers) std::cout << n << " "; std::cout << std::endl;
auto it = std::find(numbers.begin(), numbers.end(), 4);
if (it != numbers.end()) {
std::cout << "Found 4 at index: " << std::distance(numbers.begin(), it) << std::endl;
}
// Lambda predicate to find even numbers
auto even_it = std::find_if(numbers.begin(), numbers.end(), [](int n){ return n % 2 == 0; });
if (even_it != numbers.end()) {
std::cout << "Found first even number: " << *even_it << std::endl;
}
return 0;
}
Iterators are objects that provide a way to access the elements of a container sequentially. They act like pointers but are generalized to work with different container types.
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {10, 20, 30, 40};
// Using an iterator to traverse the vector
std::cout << "Elements using iterator: ";
for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// Using a const iterator for read-only access
std::cout << "Elements using const iterator: ";
for (std::vector<int>::const_iterator cit = numbers.cbegin(); cit != numbers.cend(); ++cit) {
std::cout << *cit << " ";
}
std::cout << std::endl;
// Using range-based for loop (which uses iterators internally)
std::cout << "Elements using range-based for loop: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
Move semantics is a feature introduced in C++11 that allows the transfer of ownership of resources (like dynamically allocated memory) from one object to another, avoiding unnecessary copying and improving performance, especially for objects that hold significant amounts of data.
&&
)Rvalue references are a new type of reference that can bind to rvalues (temporary objects or values that are about to expire). This allows move constructors and move assignment operators to "steal" the resources of rvalue objects.
std::move
std::move
is a utility function that converts an lvalue (a named object) into an rvalue reference. It doesn't actually move anything; it just allows the object to be treated as an rvalue so that move operations can be applied.
Move constructors and move assignment operators are special member functions that define how an object of a class should be created or assigned from an rvalue reference. Instead of copying the data, they typically transfer the ownership of the underlying resources.
#include <iostream>
#include <vector>
#include <string>
class MyString {
private:
char* data;
size_t length;
public:
// Constructor
MyString(const char* str = "") : length(std::strlen(str)) {
data = new char[length + 1];
std::strcpy(data, str);
std::cout << "Constructor called for: \"" << data << "\"" << std::endl;
}
// Copy constructor
MyString(const MyString& other) : length(other.length) {
data = new char[length + 1];
std::strcpy(data, other.data);
std::cout << "Copy constructor called for: \"" << data << "\"" << std::endl;
}
// Move constructor
MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
std::cout << "Move constructor called for: \"" << data << "\"" << std::endl;
}
// Copy assignment operator
MyString& operator=(const MyString& other) {
std::cout << "Copy assignment called for: \"" << other.data << "\"" << std::endl;
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
}
return *this;
}
// Move assignment operator
MyString& operator=(MyString&& other) noexcept {
std::cout << "Move assignment called for: \"" << other.data << "\"" << std::endl;
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// Destructor
~MyString() {
std::cout << "Destructor called for: \"" << (data ? data : "nullptr") << "\"" << std::endl;
delete[] data;
}
const char* c_str() const {
return data;
}
size_t size() const {
return length;
}
};
MyString getString() {
MyString temp("Hello from function");
return temp; // Returns by value, move constructor will be used if available
}
int main() {
MyString s1 = "Initial string";
MyString s2 = s1; // Copy constructor
MyString s3 = getString(); // Move constructor
std::cout << "s1: " << s1.c_str() << std::endl;
std::cout << "s2: " << s2.c_str() << std::endl;
std::cout << "s3: " << s3.c_str() << ", size: " << s3.size() << std::endl;
MyString s4 = "Another string";
s4 = s1; // Copy assignment
MyString s5 = "Yet another";
s5 = getString(); // Move assignment
std::cout << "s4: " << s4.c_str() << std::endl;
std::cout << "s5: " << s5.c_str() << std::endl;
MyString s6 = std::move(s1); // Explicitly move from s1
std::cout << "s6: " << s6.c_str() << std::endl;
std::cout << "s1 after move: " << (s1.c_str() ? s1.c_str() : "nullptr") << std::endl;
return 0;
}
Lambda expressions (also known as lambda functions) provide a concise way to define anonymous function objects (functors) directly in the code where they are used. They are often used with STL algorithms.
The basic syntax of a lambda expression is:
[capture-clause] (parameter-list) -> return-type {
// function body
}
capture-clause
: Specifies which variables from the surrounding scope are accessible inside the lambda and how (by value or by reference).parameter-list
: A comma-separated list of parameters (like in a regular function).return-type
: The return type of the lambda (can often be omitted, as it can be deduced by the compiler).function body
: The code to be executed when the lambda is called.[]
: Captures nothing from the surrounding scope.[=]
: Captures all automatic variables from the surrounding scope by value.[&]
: Captures all automatic variables from the surrounding scope by reference.[var]
: Captures var
by value.[&var]
: Captures var
by reference.[=, &var]
: Captures all automatic variables by value, but captures var
by reference.[&, var]
: Captures all automatic variables by reference, but captures var
by value.[this]
: Captures the this
pointer by value (available in member functions).#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int factor = 2;
// Lambda that multiplies each number by factor (capture by value)
std::cout << "Multiplied by factor (by value): ";
std::for_each(numbers.begin(), numbers.end(), [=](int& n){
n *= factor;
std::cout << n << " ";
});
std::cout << std::endl;
// Reset numbers
numbers = {1, 2, 3, 4, 5};
factor = 2;
// Lambda that multiplies each number by factor (capture by reference)
std::cout << "Multiplied by factor (by reference): ";
std::for_each(numbers.begin(), numbers.end(), [&](int& n){
n *= factor;
std::cout << n << " ";
});
std::cout << std::endl;
std::cout << "Factor after lambda (by reference): " << factor << std::endl;
// Lambda to check if a number is even
auto is_even = [](int n){ return n % 2 == 0; };
std::cout << "Is 4 even? " << std::boolalpha << is_even(4) << std::endl;
std::cout << "Is 3 even? " << std::boolalpha << is_even(3) << std::endl;
return 0;
}
These advanced features of C++ provide powerful tools for writing efficient, generic, and expressive code.