I'm trying to understand the concepts of smart pointers and templates. To get a better understanding of these, I tried to implement these two concepts on Stack
. I have created a Stack
which can hold objects of any type. For dynamic memory allocation, I have used smart pointers.
Please suggest how I could improve the code. Am I missing any cases while using smart pointers and templates? Since I'm using smart pointers, can I rest assure that free store (heap) memory will be deleted once the Stack
variable goes out of scope?
#include "stdafx.h"
#include<iostream>
#include<string>
#include<memory>
using namespace std;
// Template Node
template <typename T>
struct Node
{
T data;
shared_ptr<Node> next;
Node(T d, shared_ptr<Node> n) : data(d), next(n) {}
};
// Template Stack
// Note: T type is used to set the Node type
template <typename T>
class Stack {
private:
shared_ptr<Node<T>> head;
public:
Stack() : head(NULL) {}
Stack(const Stack<T> &rhs) = delete;
Stack& operator=(const Stack<T> &rhs) = delete;
void push(const T&);
void pop();
T top() const;
void print() const;
};
// Notice <T> after the class name, it is the syntax requirement
template <typename T>
void Stack<T>::push(const T &data) {
shared_ptr<Node<T>> temp = make_shared<Node<T>>(data,nullptr);
if (head == nullptr) {
head = temp;
}
else {
temp->next = head;
head = temp;
}
}
template <typename T>
void Stack<T>::pop() {
//shared_ptr<Node<T>> temp = head;
head = head->next;
}
template <typename T>
T Stack<T>::top() const {
return head->data;
}
template <typename T>
void Stack<T>::print() const {
// shared_ptr<Node<T>> temp;
for (auto temp = head; temp != nullptr; temp = temp->next)
cout << temp->data << '\n';
}
int main()
{
cout << "String data" << '\n';
{
Stack<string> strStack;
strStack.push("Test1");
strStack.push("Test2");
strStack.push("Test3");
strStack.print();
strStack.pop();
strStack.print();
}
cout << "Integer data" << '\n';
{
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
intStack.print();
intStack.pop();
intStack.print();
}
return 0;
}
3 Answers 3
Readability
Please put a space after the include and before the <
#include<iostream>
#include<string>
#include<memory>
Don't do this
You can read any other C++ revue on this site.
using namespace std;
See: Why is "using namespace std" considered bad practice?
Don't add useless comments
// Template Node
template <typename T>
struct Node
Thanks but I can read the code I can see it is a template and a class called Node. Useless comments are worse than no comment.
// Template Stack
// Note: T type is used to set the Node type
template <typename T>
Again you are describing what the code does. If you write nice well formatted code it should explain itself (with good method and variable names).
// Notice <T> after the class name, it is the syntax requirement
template <typename T>
void Stack<T>::push(const T &data) {
Don't explain the compiler rules. The compiler does not care nor do I.
Pass parameters by reference
Here you pass the parameters by value.
Node(T d, shared_ptr<Node> n) : data(d), next(n) {}
This means you are copying the data to the parameters than you are making a second copy of the data into the member variables.
Try this:
Node(T const& d, shared_ptr<Node>& n) : data(d), next(n) {}
You can even use move semantics. I'll cover that more in the push()
below.
Node(T&& d, shared_ptr<Node>& n) : data(std::move(d)), next(n) {}
Push
You don't need that if block
.
void Stack<T>::push(const T &data) {
shared_ptr<Node<T>> temp = make_shared<Node<T>>(data,nullptr);
if (head == nullptr) {
head = temp;
}
else {
temp->next = head;
head = temp;
}
}
You always assign temp
to head. The value assigned to temp.next
is nullptr
if the head is nullptr
or it is head. In both cases the value of head
is assigned to next.
void Stack<T>::push(const T& data) {
head = make_shared<Node<T>>(data, std::move(head));
}
You pass the data by reference, which is good. Make sure that node also takes the parameter by reference.
In some situations we also want to add move semantics to the class. So it can also be useful to add a push()
that takes an r-value reference. This allows for a much more efficient push when T
is large (like an vector).
void Stack<T>::push(T&& data) {
head = make_shared<Node<T>>(std::move(data), std::move(head));
}
Also in the vain of push. Sometimes T
is expensive to build and copy but its arguments can easily be passed. In this case it is worth allowing the user to pass the parameters to the constructor of T
and then building the T
object in place (rather than passing it around).
template<typename... Args>
void Stack<T>::push<Args>(Args...&& args) {
head = make_shared<Node<T>>(std::move(args));
}
template<typename... Args>
Node<T>::Node<Args...>(Args...&& args)
: data(args...) // Use the constructor of T
{}
Top
Here you return the result by value. This means a copy is probably generated. This could be expensive so it I would return a reference rather than a value. The user can then decide if they want to copy the reference explicitly into a value.
T Stack<T>::top() const {
return head->data;
}
Change to
T const& Stack<T>::top() const {
// ^^^^^^
return head->data;
}
Looks fine.
void Stack<T>::print() const {
// shared_ptr<Node<T>> temp;
for (auto temp = head; temp != nullptr; temp = temp->next)
cout << temp->data << '\n';
}
The thing I would change is to pass a stream as a parameter (std::cout is not the only stream you want to print to). It can default to std::cout
for backward compatibility.
Also the standard way of printing in C++ is using operator<<
so it would be nice to define that in addition.
void Stack<T>::print(std::ostream& str = std::cout) const
{
for (auto temp = head; temp != nullptr; temp = temp->next)
str << temp->data << '\n';
}
std::ostream& operator<<(std::ostream& str, Stack<T> const& value)
{
value.print(str);
return str;
}
-
\$\begingroup\$ Also, might want to take a look at your last example on push, there are some issues: it doesn't propagate the old head, you are missing a
typename T
in the template, and, sinceArgs...&&
are being deduced, you should usestd::forward
instead ofstd::move
(sinceArgs...&&
might bind to rvalue and lvalue references). \$\endgroup\$hoffmale– hoffmale2017年05月19日 18:52:19 +00:00Commented May 19, 2017 at 18:52 -
\$\begingroup\$ I deliberately left the
template<typename T>
off (to match my other examples). \$\endgroup\$Loki Astari– Loki Astari2017年05月19日 18:58:58 +00:00Commented May 19, 2017 at 18:58 -
\$\begingroup\$ Can you expand on the
forward
Vsmove
I don't understand what you are trying to say. \$\endgroup\$Loki Astari– Loki Astari2017年05月19日 18:59:39 +00:00Commented May 19, 2017 at 18:59 -
2\$\begingroup\$ basically, if you have a function template like
template<typename T> f(T&& arg) {}
, whereT
gets deduced,T&&
is a so called forwarding reference (previously universal reference). In this case,arg
can bind to rvalues and lvalues, so usingstd::move
might actually turn an lvalue reference into an rvalue reference.std::forward
only moves arg if it is a rvalue reference, and passes arg as a lvalue reference otherwise. (You might want to look up perfect forwarding.) \$\endgroup\$hoffmale– hoffmale2017年05月19日 19:05:51 +00:00Commented May 19, 2017 at 19:05 -
1\$\begingroup\$ @incomputable: that should be
U& && -> U&
so in caseT
gets deduced asU&
,arg
is of typeU&
(it's still an lvalue!) \$\endgroup\$hoffmale– hoffmale2017年05月19日 19:30:51 +00:00Commented May 19, 2017 at 19:30
#include "stdafx.h"
Assuming this is the precompiled header, you should only include it in .cpp files, not in header files. It is only used to speed up compilation.
template <typename T>
struct Node
{
T data;
shared_ptr<Node> next;
Node(T d, shared_ptr<Node> n) : data(d), next(n) {}
};
Node
s are only used in the implementation of Stack
; they are not exposed by the interface. So, Node
should be a private nested struct of Stack
.
You don't actually use shared ownership here. Each Node
instance is owned either by another Node
instance or by a Stack
instance. Therefore using shared_ptr
is a premature pessimization here. You should use unique_ptr
instead. The same holds for Stack::head
.
template <typename T>
class Stack {
//...
public:
Stack() : head(NULL) {}
Stack(const Stack<T> &rhs) = delete;
Stack& operator=(const Stack<T> &rhs) = delete;
//...
};
Use nullptr
instead of NULL
, it provides better type safety. Also, the default constructor of shared_ptr
(and unique_ptr
) already initialize the pointer to nullptr
. So, you don't need to actually implement the default constructor of Stack
, but can just define it as default.
Stack() = default;
I don't see any reason why Stack
should not be copyable or asignable. Don't be lazy and implement these functions when you write a class is is supposed to be reused. While your at it, throw in the move constructor and move assignment operator too, unless your compiler will automatically generate them appropriately.
Smart pointers can seriously bite you when you try to use them in the implementation of a node-based container like a stack. If your container contains enough elements, then its destructor can easily overflow the call stack by recursively calling the smart pointer destructor to too great a depth of recursion.
Say your container contains 1,000 elements. When it is destroyed, its destructor calls the destructor of its head
smart pointer, and that destructor calls the destructor of its next
smart pointer, and that destructor calls the destructor of its next
smart pointer, and so on, until the depth of function call recursion surpasses the capacity of the call stack and it overflows.
This can be worked around by defining a destructor for your container that repeatedly calls pop
until the container is empty. (And a public is_empty
member function is also a pretty good idea.)