-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
211: Practice Exercise: Building a Template-Based Generic Container
When you first realize you need a container that can hold "anything," your instinct might be to find a way to erase the type. I've seen this a lot with developers coming from languages like Java or C#, or even those trying to write C++ like it's C. They want a single class that can handle integers, strings, or custom User objects without writing the same logic three times. The immediate, naive impulse is to use void* or a common base class.
The Danger of Type Erasure
Imagine we're building a simple FixedArray. The "naive" way to make it generic is to store a pointer to void. You'd allocate a block of memory and just cast whatever the user gives you into that void pointer. It looks flexible on paper, but in practice, it's a nightmare. Since void* has no size or type information, you have to manually track the size of the elements and, more importantly, you have to remember exactly what you put in there to cast it back correctly when you retrieve it.
// The "Don't Do This" Approach
class NaiveContainer {
void** data;
int capacity;
public:
void set(int index, void* value) { data[index] = value; }
void* get(int index) { return data[index]; }
};
// Usage
NaiveContainer container;
container.set(0, new int(10));
// I hope I remember this was an int, otherwise... boom.
int val = *static_cast<int*>(container.get(0));
The problem here isn't just the verbosity of the casts; it's that you've completely bypassed the C++ type system. The compiler is now blind. If you accidentally push a std::string and try to pull it out as an int, the compiler won't say a word. You'll just get a segmentation fault or, even worse, silent memory corruption that takes three days to debug. I've spent far too many weekends chasing bugs born from this exact pattern.
Leveraging Template Instantiation
The better way—the C++ way—is to use templates. Instead of trying to hide the type from the compiler, we tell the compiler to generate a specific version of the class for every type we actually use. When you define a template <typename T>, you aren't writing a class; you're writing a blueprint for a class.
template <typename T>
class GenericContainer {
T* data;
int capacity;
public:
GenericContainer(int size) : capacity(size) {
data = new T[size];
}
~GenericContainer() { delete[] data; }
void set(int index, T value) { data[index] = value; }
T get(int index) { return data[index]; }
};
Now, the compiler handles the heavy lifting. If you declare a GenericContainer<int>, the compiler effectively writes a version of the class where every T is replaced by int. This gives us absolute type safety. If you try to put a string into an integer container, the code won't even compile. You get the error at build time, which is the cheapest possible place to find a bug.
The Cost of Genericity
Now, I should be honest about the trade-offs. Templates aren't free. Because the compiler generates a new class for every unique type you use, you can run into "code bloat." If you use GenericContainer with 50 different types, the compiler generates 50 different versions of those methods in your binary. In most modern applications, this is a negligible cost compared to the massive gain in safety and performance (since we've eliminated the need for runtime casting).
Another quirk you'll notice is that you can't easily split template classes into .h and .cpp files the way you do with regular classes. Since the compiler needs the full definition to instantiate the type, the implementation usually has to live entirely in the header. It feels messy at first, but it's a necessary part of how C++ achieves this kind of zero-overhead abstraction.
📋 Practical Task
Exercise: Implementing a Template-Based Fixed-Size Stack with Bounds Checking
Your goal is to build a generic FixedStack class. This container should allow the user to specify the type of data it holds and a maximum capacity at runtime, but it must ensure that no invalid memory access occurs during push or pop operations.
Requirements:
- Create a template class
FixedStack<T>. - Implement a constructor that takes an
int capacityand allocates an internal array of typeT. - Implement a
push(T value)method. If the stack is full, it should throw astd::out_of_rangeexception. - Implement a
pop()method. If the stack is empty, it should throw astd::out_of_rangeexception. - Implement a
peek()method to return the top element without removing it. - Ensure the destructor properly cleans up the dynamically allocated array to prevent memory leaks.
Test Case: In your main function, instantiate one FixedStack<int> and one FixedStack<std::string>. Push three elements into the integer stack, pop one, and then intentionally try to push more elements than the capacity allows to verify your exception handling works.
There are no comments for now.