In safety-critical firmware, dynamic memory allocation (heap) is often forbidden due to fragmentation risks. Data structures like stacks and queues must have their maximum size fixed at compile-time to ensure deterministic memory usage.
Your task is to implement a Class Template named StaticStack. It must accept two template parameters:
typename T: The type of data to store.int Capacity: The maximum number of elements (Non-Type Template Parameter).The class must encapsulate a raw array T data[Capacity] and a top_index. Implement the following methods:
void push(T val): Add element. If full, print "Stack Overflow" and ignore.void pop(): Remove top element. If empty, print "Stack Underflow" and ignore.T peek(): Return top element. If empty, return 0 (or default construct T()) and print nothing (error handling logic varies, but for this exercise, assume safe usage or check empty first).bool isEmpty(): Returns true if empty.Program Flow:
main function (provided) instantiates a StaticStack<int, 3> (Capacity = 3).N (number of commands).N times.command.val and call push(val).pop().peek().Input Format:
N.N lines: Command string, followed by val only if command is "PUSH".Output Format:
PUSH: No output (unless overflow).POP: No output (unless underflow).PEEK: Print Top: <value>.EMPTY: Print Empty: Yes or Empty: No.Stack Overflow or Stack Underflow exactly.Example:
Example 1
Input:
6
PUSH 10
PUSH 20
PUSH 30
PUSH 40
PEEK
POPOutput:
Stack Overflow
Top: 30(Explanation: Stack size is 3. Pushing 10, 20, 30 fills it. Pushing 40 triggers Overflow. Peek shows 30. Pop removes 30.)
Constraints:
N range: 1 to 20int, but class must be a template.std::vector or new/malloc.
Templates allow you to write a single "blueprint" for a function or class that can work with any data type. Instead of writing swap_int, swap_float, and swap_double, you write one swap<T> function.
This is Compile-Time Polymorphism. The compiler sees how you use the template and generates (instantiates) the specific code for those types. It’s like a "Smart Macro" that respects type safety and scope.
1. Function Templates
Write logic once, use it for different types.
// Template Declaration
// 'T' is a placeholder for a type (int, float, struct...)
template <typename T>
void swap_values(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swap_values(x, y); // Compiler generates: void swap_values(int&, int&)
float f1 = 1.5, f2 = 4.5;
swap_values(f1, f2); // Compiler generates: void swap_values(float&, float&)
}2. Class Templates
Crucial for creating generic data structures (Buffers, Queues, Linked Lists) where the logic is the same regardless of what data is stored inside.
// A Circular Buffer that can hold 'T' type objects
// 'Size' is a Non-Type Template Parameter (Constant Integer)
template <typename T, int Size>
class RingBuffer {
private:
T buffer[Size]; // Array size fixed at compile time
int head = 0;
public:
void push(T val) {
buffer[head] = val;
head = (head + 1) % Size;
}
};
// Usage
RingBuffer<int, 64> adc_buf; // Buffer of 64 integers
RingBuffer<float, 10> sensor_avg; // Buffer of 10 floats
RingBuffer<Packet, 128> uart_rx; // Buffer of 128 custom StructsTemplates do not exist in the final binary until they are used.
When you write RingBuffer<int, 64>, the compiler acts like a code generator:
It takes the "Blueprint".
It replaces T with int and Size with 64.
It compiles that new specific class.
Code | Compiler Action | Memory Usage |
|---|---|---|
| Generates sort_int(int* arr) | Code size increases by size of function. |
| Generates sort_float(float* arr) | Code size increases again. |
| Reuses existing sort_int | No new code generated. |
Sometimes generic code works for 99% of types, but fails for one specific type (like bool or char*). You can provide a special version for that type.
// Generic
template <typename T>
T add(T a, T b) { return a + b; }
// Specialization for 'char' (e.g., to prevent overflow or change logic)
template <>
char add<char>(char a, char b) {
// Saturated addition logic for 8-bit audio
int res = a + b;
if (res > 127) return 127;
return (char)res;
}
Type-Safe Drivers (Zero Cost Abstraction)
In C, drivers often use void* buffers and require casting (dangerous) or macros (hard to debug).
Templates give you type safety with zero runtime overhead. The compiler resolves types during compilation, so the resulting assembly is as fast as hand-written C code.
Compile-Time Configuration
You can use "Non-Type Template Parameters" (integers, pointers) to configure hardware.
template <uint32_t BaseAddr>
class GPIO {
public:
static void setHigh() {
*(volatile uint32_t*)(BaseAddr) = 1;
}
};
using LED_Port = GPIO<0x40021000>;
// LED_Port::setHigh() compiles to a direct memory write instruction!Avoids malloc
Standard containers (std::vector) use the Heap (malloc). In embedded, we hate malloc.
With templates, you can pass the size as a parameter (RingBuffer<int, 64>), allowing the buffer to be allocated on the Stack or BSS (Static Memory).
The Fear: "Templates generate a copy of the code for every type, filling up my Flash memory."
The Reality: Yes, vector<int> and vector<float> are two separate code blocks.
However, if you manually wrote IntVector and FloatVector structs in C, you would have the exact same amount of code.
Optimization: If different template instantiations result in identical assembly (e.g., MyClass<unsigned int> vs MyClass<int> on some architectures), the linker can sometimes merge them.
Pitfall | Details |
|---|---|
❌ Header Definitions | Template code must be implemented in the Header File ( Why? The compiler needs to see the entire source code to generate the specific version when it compiles |
❌ Cryptic Error Messages | If you make a typo in a template, the compiler vomits 100 lines of error garbage. Tip: Always scroll to the very top error message; it usually points to the actual line. Ignore the "instantiated from here" chain below it. |
❌ Bloat via Permutation | Be careful with integer parameters.
|
✅ |
|