Loading...

Go Back

Next page
Go Back Course Outline

C++ Full Course


Advanced Features in C++

Advanced C++ Features

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

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

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;
                        }
                        
Max of 5 and 10: 10
Max of 5.2 and 3.7: 5.2
Max of 'a' and 'c': c
Max of strings 'hello' and 'world': world

Class Templates

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;
                        }
                        
Integer vector size: 2, Element at 0: 10
String vector size: 2, Element at 1: banana

Template Specialization

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;
                        }
                        
Generic print: 123
String print: "hello"
Generic print: 3.14

STL (Standard Template Library)

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

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;
                        }
                        
Vector: 1 2 3
List: 3 2 1
Map: apple:1 banana:2
Unordered Map: banana:2 apple:1
Set: 1 2 3
Unordered Set: 2 1 3


Algorithms

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;
                        }
                        
Sorted (ascending): 1 2 4 5 8
Sorted (descending): 8 5 4 2 1
Found 4 at index: 2
Found first even number: 8

Iterators

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.

  • Input iterators: Can be used to read elements from a sequence.
  • Output iterators: Can be used to write elements to a sequence.
  • Forward iterators: Combine the functionality of input and output iterators and can traverse a sequence multiple times in the same direction.
  • Bidirectional iterators: Can move both forward and backward through a sequence.
  • Random-access iterators: Provide all the capabilities of bidirectional iterators and also allow direct access to any element using pointer arithmetic.
#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;
                        }
                        
Elements using iterator: 10 20 30 40
Elements using const iterator: 10 20 30 40
Elements using range-based for loop: 10 20 30 40

Move Semantics (C++11+)

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 (&&)

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

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;
                            }
                            


Constructor called for: "Initial string"
Copy constructor called for: "Initial string"
Constructor called for: "Hello from function"
Move constructor called for: "Hello from function"
Destructor called for: "Hello from function"
s1: Initial string
s2: Initial string
s3: Hello from function, size: 19
Constructor called for: "Another string"
Copy assignment called for: "Initial string"
Destructor called for: "Another string"
Constructor called for: "Yet another"
Constructor called for: "Hello from function"
Move assignment called for: "Hello from function"
Destructor called for: "Hello from function"
Destructor called for: "Yet another"
s4: Initial string
s5: Hello from function
Move constructor called for: "Initial string"
Destructor called for: "Initial string"
s6: Initial string
s1 after move: nullptr
Destructor called for: "Initial string"
Destructor called for: "Initial string"
Destructor called for: "Hello from function"
Destructor called for: "Initial string"

Lambda Expressions (C++11+)

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.

Syntax

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.

Capture Clauses

  • []: 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).

Example of Lambda Expressions

#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;
                            }
                            
Multiplied by factor (by value): 2 4 6 8 10
Multiplied by factor (by reference): 2 4 6 8 10
Factor after lambda (by reference): 4
Is 4 even? true
Is 3 even? false

These advanced features of C++ provide powerful tools for writing efficient, generic, and expressive code.

Go Back

Next page