C++ Templates
C++ Templates
Section titled “C++ Templates”Templates are a fundamental feature of C++ enabling generic programming. They allow you to write code (functions or classes) that can operate on different data types without needing to rewrite the code for each type.
Think of a template as a blueprint. You define the structure or algorithm once, using placeholder type names. When you use the template with a specific type (like int or std::string), the compiler generates the actual code for that specific type based on the blueprint.
The C++ Standard Library heavily relies on templates. Containers like std::vector, std::list, and std::map, as well as algorithms like std::sort and std::find, are all template-based. This allows you to have std::vector<int>, std::vector<double>, or std::vector<MyClass> using the same underlying std::vector template code.
Function Templates:
Section titled “Function Templates:”Function templates define a generic function that can operate on various data types. The syntax typically looks like this:
template <typename T> // or template <class T>return_type function_name(parameter_list) { // Function body using type T}Here, typename T (or class T) declares T as a template parameter, a placeholder for a type. You can use T within the function signature and body as if it were a specific type.
Example: A function template to find the maximum of two values.
#include <iostream>#include <string>
// Define a function template named 'maximum'// It takes two constant references of type T and returns a constant reference of type Ttemplate <typename T>const T& maximum(const T& a, const T& b) { // Assumes type T supports the less than operator (<) return (a < b) ? b : a;}
int main() { int i = 39; int j = 20; // Compiler generates maximum<int>(const int&, const int&) std::cout << "Max(i, j): " << maximum(i, j) << std::endl;
double f1 = 13.5; double f2 = 20.7; // Compiler generates maximum<double>(const double&, const double&) std::cout << "Max(f1, f2): " << maximum(f1, f2) << std::endl;
std::string s1 = "Hello"; std::string s2 = "World"; // Compiler generates maximum<std::string>(const std::string&, const std::string&) std::cout << "Max(s1, s2): " << maximum(s1, s2) << std::endl;
// You can also explicitly specify the type: // std::cout << "Max(i, j): " << maximum<int>(i, j) << std::endl;
return 0;}Output:
Max(i, j): 39Max(f1, f2): 20.7Max(s1, s2): WorldThe compiler deduces the type T from the arguments passed to maximum. Using const T& for parameters is efficient as it avoids copying potentially large objects.
Class Templates:
Section titled “Class Templates:”Class templates allow you to define generic classes. The syntax is similar to function templates:
template <typename T1, typename T2, ...> // or class T1, class T2, ...class ClassName { // Class members using T1, T2, etc.private: T1 member1; T2 member2;public: ClassName(T1 val1, T2 val2); void someMethod(T1 arg); // ...};Here, T1, T2, etc., are placeholder types specified when an object of the class is created.
Example: A generic Stack class template.
#include <iostream>#include <vector>#include <stdexcept> // For std::out_of_range#include <string>#include <utility> // For std::move
template <typename T>class Stack {private: std::vector<T> elems; // Internal storage using std::vector
public: // Push element by copying void push(const T& elem) { elems.push_back(elem); }
// Push element by moving (C++11) void push(T&& elem) { elems.push_back(std::move(elem)); // Efficient for movable types }
// Pop (remove) the top element void pop() { if (empty()) { throw std::out_of_range("Stack<>::pop(): empty stack"); } elems.pop_back(); }
// Return a const reference to the top element const T& top() const { if (empty()) { throw std::out_of_range("Stack<>::top(): empty stack"); } return elems.back(); }
// Return a mutable reference to the top element T& top() { if (empty()) { throw std::out_of_range("Stack<>::top(): empty stack"); } return elems.back(); }
// Check if the stack is empty [[nodiscard]] bool empty() const noexcept { // C++11 noexcept, C++17 nodiscard return elems.empty(); }
// Get the current size of the stack [[nodiscard]] size_t size() const noexcept { return elems.size(); }};
int main() { try { Stack<int> intStack; // Stack of integers Stack<std::string> stringStack; // Stack of strings
// Manipulate int stack std::cout << "Pushing 7 onto intStack..." << std::endl; intStack.push(7); std::cout << "Top element: " << intStack.top() << std::endl; std::cout << "Pushing 15 onto intStack..." << std::endl; intStack.push(15); std::cout << "Top element: " << intStack.top() << std::endl; std::cout << "Popping intStack..." << std::endl; intStack.pop(); std::cout << "Top element: " << intStack.top() << std::endl;
// Manipulate string stack std::cout << "\nPushing \"hello\" onto stringStack..." << std::endl; stringStack.push("hello"); std::cout << "Top element: " << stringStack.top() << std::endl; std::string world = "world"; std::cout << "Pushing \"world\" onto stringStack..." << std::endl; stringStack.push(std::move(world)); // Move semantics example std::cout << "Top element: " << stringStack.top() << std::endl; std::cout << "Is 'world' string empty now? " << (world.empty() ? "Yes" : "No") << std::endl;
std::cout << "Popping stringStack twice..." << std::endl; stringStack.pop(); stringStack.pop();
// This will throw an exception std::cout << "Attempting to pop empty stringStack..." << std::endl; stringStack.pop();
} catch (const std::out_of_range& ex) { std::cerr << "\nCaught Exception: " << ex.what() << std::endl; return 1; // Indicate error } catch (const std::exception& ex) { std::cerr << "\nCaught general exception: " << ex.what() << std::endl; return 1; }
return 0;}Example Output:
Pushing 7 onto intStack...Top element: 7Pushing 15 onto intStack...Top element: 15Popping intStack...Top element: 7
Pushing "hello" onto stringStack...Top element: helloPushing "world" onto stringStack...Top element: worldIs 'world' string empty now? YesPopping stringStack twice...Attempting to pop empty stringStack...
Caught Exception: Stack<>::pop(): empty stackNote: When defining member functions of a class template outside the class definition, you need to repeat the template declaration and use the scope resolution operator with the template parameters (e.g., template <typename T> void Stack<T>::pop() { ... }).
Advanced Template Features:
Section titled “Advanced Template Features:”Modern C++ has introduced powerful template features like:
- Variadic Templates (C++11): Allow templates to accept a variable number of arguments.
- Fold Expressions (C++17): Provide a concise syntax for applying an operator to all elements of a parameter pack (from variadic templates).
- Class Template Argument Deduction (CTAD) (C++17): Allows the compiler to deduce template arguments for class templates from constructor arguments in many cases (e.g.,
std::pair p(1, "hello");instead ofstd::pair<int, const char*> p(1, "hello");). - Concepts (C++20): Allow specifying constraints on template parameters, improving error messages and enabling template overloading based on requirements.