Appreciate your thoughts and feedback...
Stack.h
// Stack implementation
// Push/Pop - Last In First Out
//
#pragma once
#include <memory>
template<class T>
class CStack
{
public:
enum { DEFAULT_SIZE = 5 };
CStack();
CStack(const CStack& rhs);
CStack(CStack&& rhs);
CStack& operator=(const CStack& rhs);
CStack& operator=(CStack&& rhs);
~CStack();
bool Empty() const;
void Pop();
bool Push(const T& t);
const T& Top() const;
T& Top();
private:
bool Grow();
private:
std::allocator<T> m_Allocator;
T* m_pBase;
int m_iCapacity;
int m_iSize;
};
template<class T>
CStack<T>::CStack()
: m_iCapacity(DEFAULT_SIZE)
, m_iSize(0)
, m_pBase(nullptr)
{
// allocate some memory for out stack
try {
m_pBase = m_Allocator.allocate(DEFAULT_SIZE);
} catch (const std::bad_alloc& e) {
// do some error logging
std::cerr << "Allocation Failed: " << e.what() << std::endl;
// rethrow exeception so client of this class can also handle it in whatever way they choose
throw;
}
}
template<class T>
CStack<T>::CStack(const CStack& rhs)
{
try {
// allocate enough memory to hold stack
m_pBase = m_Allocator.allocate(rhs.m_iCapacity);
} catch (const std::bad_alloc& e) {
// do some error logging
std::cerr << "Allocation Failed: " << e.what() << std::endl;
// rethrow exeception so client of this class can also handle it in whatever way they choose
throw;
}
// copy across the stack
// this performs a deep copy
for (int i = 0; i < rhs.m_iSize; ++i) {
// copy across the T that is in the corresponding place
m_Allocator.construct(m_pBase + i, *(rhs.m_pBase + i));
}
// copy across size and capacity
m_iCapacity = rhs.m_iCapacity;
m_iSize = rhs.m_iSize;
}
template<class T>
CStack<T>::CStack(CStack&& rhs)
: m_pBase(std::move(rhs.m_pBase))
, m_iCapacity(rhs.m_iCapacity)
, m_iSize(rhs.m_iSize)
{
rhs.m_iCapacity = 0;
rhs.m_iSize = 0;
rhs.m_pBase = nullptr;
}
template<class T>
CStack<T>& CStack<T>::operator=(const CStack& rhs)
{
// check for self-assignment
if (this == &rhs) {
return *this;
}
// perform copy-swap idiom
// create deep copy
CStack<T> copy(rhs);
// swap
std::swap(m_pBase, copy.m_pBase);
std::swap(m_iCapacity, copy.m_iCapacity);
std::swap(m_iSize, copy.m_iSize);
// copy will be destructed when this function exits
return *this;
}
template<class T>
CStack<T>& CStack<T>::operator=(CStack&& rhs)
{
// check for self move
if (this == &rhs) {
return *this;
}
// take ownership of lhs memory
T* pOldBase = m_pBase;
int m_iOldCapacity = m_iCapacity;
int iOldSize = m_iSize;
// move across ownership of rhs memory to lhs
m_pBase = std::move(rhs.m_pBase);
m_iCapacity = rhs.m_iCapacity;
m_iSize = rhs.m_iSize;
// make sure rhs points to null
rhs.m_pBase = nullptr;
rhs.m_iCapacity = 0;
rhs.m_iSize = 0;
// now that lhs points to rhs
// destruct old lhs
for (int i = 0; i < iOldSize; ++i) {
m_Allocator.destroy(pOldBase + i);
}
m_Allocator.deallocate(pOldBase, m_iOldCapacity);
return *this;
}
template<class T>
CStack<T>::~CStack()
{
// check for valid pointer
// stack could have been moved-from
// and so the pointer will be null
if (m_pBase) {
// destory any objects on the stack
for (int i = 0; i < m_iSize; ++i) {
m_Allocator.destroy(m_pBase + i);
}
// deallocate any memory
m_Allocator.deallocate(m_pBase, m_iCapacity);
}
}
// no op on an empty stack
// use Empty() to check if stack is empty
template<class T>
void CStack<T>::Pop()
{
// destroy the top element
m_Allocator.destroy(m_pBase + (m_iSize - 1));
// reduce the size of the stack
--m_iSize;
}
template<class T>
bool CStack<T>::Push(const T& t)
{
if (!m_pBase) return false;
// does the stack need to grow
if ((m_iSize + 1) > m_iCapacity) {
// increase the size of the stack
if (!Grow()) {
return false;
}
}
// increase the size to include new element
++m_iSize;
// construct the pushed object in the allocated memory
m_Allocator.construct(m_pBase + (m_iSize - 1), t);
return true;
}
// returns a const reference to the top element in the stack
// it is up to the caller to make sure stack is non-empty using Empty() function
template<class T>
const T& CStack<T>::Top() const
{
return *(m_pBase + (m_iSize - 1));
}
// returns a const reference to the top element in the stack
// it is up to the caller to make sure stack is non-empty using Empty() function
template<class T>
T& CStack<T>::Top()
{
return *(m_pBase + (m_iSize - 1));
}
// checks size of stack and returns true (if empty) or false
template<class T>
bool CStack<T>::Empty() const
{
return m_iSize <= 0;
}
// increases the size of the stack by allocating a larger block of memory
// and copying the stacks elements to it.
template<class T>
bool CStack<T>::Grow()
{
// not that stack should ever grow this big but just incase
// avoid overflow
if (m_iCapacity >= (std::numeric_limits<int>::max() / 3)) {
return false;
}
// create a new stack twice the size
int iNewCapacity = m_iCapacity * 2;
T* pTempBase = nullptr;
try {
// allocate some unintialised memory
pTempBase = m_Allocator.allocate(iNewCapacity);
} catch (const std::bad_alloc& e) {
// do some error logging
std::cerr << "Allocation Failed: " << e.what() << std::endl;
// rethrow exeception so client of this class can also handle it in whatever way they choose
throw;
}
for (int i = 0; i < m_iSize; ++i) {
// construct a copy in place in new allocation block
m_Allocator.construct(pTempBase + i, *(m_pBase + i));
}
// now that we have a exact copy of the stack
// swap pointers
std::swap(m_pBase, pTempBase);
// deconstruct each object in the original stack
for (int i = 0; i < m_iSize; ++i) {
// construct a copy in place in new allocation
m_Allocator.destroy(pTempBase + i);
}
// deallocate memory
m_Allocator.deallocate(pTempBase, m_iCapacity);
// set the new capcity
m_iCapacity = iNewCapacity;
return true;
}
Code can be found on Github
Updated version with code review input added here New Github
Some of my own notes to help you understand my choices..
I left it up to the user of my stack to check if a stack is empty before trying to pop an item. This is how STL does it. Interested to hear your thoughts. Maybe throwing an exception is better when a pop on an empty stack is attempted?
Pop()
does not return anything. You use Top()
and Pop()
to remove an item of the stack. Again STL style.
I am using an allocator to avoid unnecessary calls to constructors in empty stack slots. Couldn't work out any other way to achieve this. Consider an empty stack of 5 items. Without allocating a block of uninitialised memory, the constructor for type T
would be called 5 times.
I am rethrowing any exceptions my stack produces. Better to just log a message to cerr
and consume the exception (not rethrow)?
A public Resize
function taking a size is better than Grow()
? Someone might want to reduce the memory footprint of their stack manually.
C
prefix means forCStack
? It appears to be a Microsoft thing that people are using without explanation. Classes don't need to start with 'C'. \$\endgroup\$std::stack
does not? \$\endgroup\$