C++ Updated Stack Code
$begingroup$
The original question can be seen here: Stack implementation in C++ using linked list
Changes (with the help of Martin):
- Added copy constructor and assignment operator overload
- Changed return type of peek() to const T& rather than T (this prevents unnecessary copying of data)
- Changed return type of pop() to void; it now only removes the top item rather than returning it on top. The user can call peek() and then call pop() to retrieve and then delete the item. This means we don't have to return T by value, and also maintains the "Strong Exception Guarantee".
- Fixed a bug in pop() where the new head is deleted rather than the old one
#ifndef TEST_STACK_H
#define TEST_STACK_H
#include <stdexcept>
template <class T>
class stack {
struct node {
T data;
node* previous;
node(T data, node *previous) : data(data), previous(previous) {}
};
node* head = nullptr;
int size = 0;
int max = -1; // -1 so isFull() == false when default constructor used
public:
stack() = default;
stack(int max) {
if (max <= 0) throw std::out_of_range("stack size must be > 0");
this->max = max;
}
// copy constructor
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
// assignment operator
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
~stack() {
node* n = head;
while (n != nullptr) {
node* previous = n->previous;
delete n;
n = previous;
}
}
void push(const T &object) {
if (isFull()) throw std::overflow_error("cannot push to a full stack");
head = new node(object, head);
++size;
}
const void pop() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
node* old = head;
head = head->previous;
--size;
delete old;
}
T peek() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return head->data;
}
int getSize() {
return size;
}
bool isFull() {
return size == max;
}
bool isEmpty() {
return head == nullptr;
}
void swap(stack& other) noexcept
{
using std::swap;
swap(head, other.head);
swap(size, other.size);
swap(max, other.max);
}
private:
node* copyList(node* l)
{
if (l == nullptr) {
return nullptr;
}
return new node{l->data, copyList(l->previous)};
}
};
#endif
c++ beginner stack
$endgroup$
add a comment |
$begingroup$
The original question can be seen here: Stack implementation in C++ using linked list
Changes (with the help of Martin):
- Added copy constructor and assignment operator overload
- Changed return type of peek() to const T& rather than T (this prevents unnecessary copying of data)
- Changed return type of pop() to void; it now only removes the top item rather than returning it on top. The user can call peek() and then call pop() to retrieve and then delete the item. This means we don't have to return T by value, and also maintains the "Strong Exception Guarantee".
- Fixed a bug in pop() where the new head is deleted rather than the old one
#ifndef TEST_STACK_H
#define TEST_STACK_H
#include <stdexcept>
template <class T>
class stack {
struct node {
T data;
node* previous;
node(T data, node *previous) : data(data), previous(previous) {}
};
node* head = nullptr;
int size = 0;
int max = -1; // -1 so isFull() == false when default constructor used
public:
stack() = default;
stack(int max) {
if (max <= 0) throw std::out_of_range("stack size must be > 0");
this->max = max;
}
// copy constructor
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
// assignment operator
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
~stack() {
node* n = head;
while (n != nullptr) {
node* previous = n->previous;
delete n;
n = previous;
}
}
void push(const T &object) {
if (isFull()) throw std::overflow_error("cannot push to a full stack");
head = new node(object, head);
++size;
}
const void pop() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
node* old = head;
head = head->previous;
--size;
delete old;
}
T peek() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return head->data;
}
int getSize() {
return size;
}
bool isFull() {
return size == max;
}
bool isEmpty() {
return head == nullptr;
}
void swap(stack& other) noexcept
{
using std::swap;
swap(head, other.head);
swap(size, other.size);
swap(max, other.max);
}
private:
node* copyList(node* l)
{
if (l == nullptr) {
return nullptr;
}
return new node{l->data, copyList(l->previous)};
}
};
#endif
c++ beginner stack
$endgroup$
add a comment |
$begingroup$
The original question can be seen here: Stack implementation in C++ using linked list
Changes (with the help of Martin):
- Added copy constructor and assignment operator overload
- Changed return type of peek() to const T& rather than T (this prevents unnecessary copying of data)
- Changed return type of pop() to void; it now only removes the top item rather than returning it on top. The user can call peek() and then call pop() to retrieve and then delete the item. This means we don't have to return T by value, and also maintains the "Strong Exception Guarantee".
- Fixed a bug in pop() where the new head is deleted rather than the old one
#ifndef TEST_STACK_H
#define TEST_STACK_H
#include <stdexcept>
template <class T>
class stack {
struct node {
T data;
node* previous;
node(T data, node *previous) : data(data), previous(previous) {}
};
node* head = nullptr;
int size = 0;
int max = -1; // -1 so isFull() == false when default constructor used
public:
stack() = default;
stack(int max) {
if (max <= 0) throw std::out_of_range("stack size must be > 0");
this->max = max;
}
// copy constructor
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
// assignment operator
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
~stack() {
node* n = head;
while (n != nullptr) {
node* previous = n->previous;
delete n;
n = previous;
}
}
void push(const T &object) {
if (isFull()) throw std::overflow_error("cannot push to a full stack");
head = new node(object, head);
++size;
}
const void pop() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
node* old = head;
head = head->previous;
--size;
delete old;
}
T peek() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return head->data;
}
int getSize() {
return size;
}
bool isFull() {
return size == max;
}
bool isEmpty() {
return head == nullptr;
}
void swap(stack& other) noexcept
{
using std::swap;
swap(head, other.head);
swap(size, other.size);
swap(max, other.max);
}
private:
node* copyList(node* l)
{
if (l == nullptr) {
return nullptr;
}
return new node{l->data, copyList(l->previous)};
}
};
#endif
c++ beginner stack
$endgroup$
The original question can be seen here: Stack implementation in C++ using linked list
Changes (with the help of Martin):
- Added copy constructor and assignment operator overload
- Changed return type of peek() to const T& rather than T (this prevents unnecessary copying of data)
- Changed return type of pop() to void; it now only removes the top item rather than returning it on top. The user can call peek() and then call pop() to retrieve and then delete the item. This means we don't have to return T by value, and also maintains the "Strong Exception Guarantee".
- Fixed a bug in pop() where the new head is deleted rather than the old one
#ifndef TEST_STACK_H
#define TEST_STACK_H
#include <stdexcept>
template <class T>
class stack {
struct node {
T data;
node* previous;
node(T data, node *previous) : data(data), previous(previous) {}
};
node* head = nullptr;
int size = 0;
int max = -1; // -1 so isFull() == false when default constructor used
public:
stack() = default;
stack(int max) {
if (max <= 0) throw std::out_of_range("stack size must be > 0");
this->max = max;
}
// copy constructor
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
// assignment operator
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
~stack() {
node* n = head;
while (n != nullptr) {
node* previous = n->previous;
delete n;
n = previous;
}
}
void push(const T &object) {
if (isFull()) throw std::overflow_error("cannot push to a full stack");
head = new node(object, head);
++size;
}
const void pop() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
node* old = head;
head = head->previous;
--size;
delete old;
}
T peek() {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return head->data;
}
int getSize() {
return size;
}
bool isFull() {
return size == max;
}
bool isEmpty() {
return head == nullptr;
}
void swap(stack& other) noexcept
{
using std::swap;
swap(head, other.head);
swap(size, other.size);
swap(max, other.max);
}
private:
node* copyList(node* l)
{
if (l == nullptr) {
return nullptr;
}
return new node{l->data, copyList(l->previous)};
}
};
#endif
c++ beginner stack
c++ beginner stack
edited Jan 19 at 17:41
Jamal♦
30.3k11117227
30.3k11117227
asked Jan 19 at 14:52
Samueljh1Samueljh1
355
355
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
$begingroup$
#include <stdexcept>
And either <utility>
or <algorithm>
, for std::swap
.
stack(int max) {
I very strongly recommend you make this constructor explicit
. Right now it's a non-explicit (implicit) conversion, which means you're permitting your users to convert integers to stacks implicitly:
void foo(stack<int> s);
void bar() {
foo(42); // compiles and calls foo with stack<int>(42)
}
The general rule (at least for user codebases) is "make everything explicit
unless you have some specific reason that it needs to be implicit." So you should make your node
constructor explicit
as well.
(I say "at least for user codebases" because the STL itself doesn't follow that principle — partly because the principle wasn't understood in 1998, and partly due to differences of opinion among the authors. "STL style" would be to make stack(int)
explicit because it's a one-argument constructor, but leave all zero- and two-argument constructors implicit. I recommend against doing that.)
int max = -1; // -1 so isFull() == false when default constructor used
This comment doesn't help me understand. I would understand INT_MAX
, but -1
just looks like it's breaking the class invariant enforced by the one-argument constructor:
if (max <= 0) throw std::out_of_range("stack size must be > 0");
Looking at these two lines in isolation, actually, I briefly wondered "wait, doesn't that always throw? You don't initialize max
before that if
, so it would have its default value, which is less than zero..." But then I realized that the name max
here is overloaded: the max
in the if
statement is testing the function parameter, not the data member.
To eliminate confusion about name overloading, I strongly recommend that you sigil each of your data members with a trailing underscore: max_
, size_
, head_
.
Then you don't have to write this->
when you access a member:
this->max = max;
can become just
max_ = max;
You don't need the disambiguating this->
, because there's no confusion anymore about which max
/max_
you mean.
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
Do you see the typo?
Write some unit tests for your code! In particular, now that you've spotted a bug, write a unit test that would have caught this bug and commit it as part of the bugfix. That's called a "regression test."
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
FYI, the copy-and-swap idiom can be written more concisely if you want to:
stack& operator=(stack const& rhs)
{
stack(rhs).swap(*this);
return *this;
}
const void pop() {
The const
here is useless; remove it. (I wonder if it was a mistake for something similar — void pop() const
? constexpr void pop()
? but neither of those makes sense either.)
T peek() {
OTOH, this method should be const:
T peek() const {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return std::as_const(head->data);
}
I threw in an as_const
just to illustrate complete paranoia. We don't know what type T
is, right? So when you construct the return value T
, which constructor do you want to call — T(T&)
or T(const T&)
? Write a unit test for a type T
where it makes a difference, and see what happens.
getSize()
, isFull()
, and isEmpty()
should all be const
methods as well.
void swap(stack& other) noexcept
Good! You also do the using std::swap;
dance correctly — although, since you're not swapping anything but ints and pointers, the dance is unnecessary in this case, but hey it's good practice.
You did forget, though, that if you want the using std::swap;
dance to work for anyone else who uses your stack
class, you'll need to provide that free ADL function swap
. So we add an inline friend:
friend void swap(stack& a, stack& b) noexcept {
a.swap(b);
}
Now
using std::swap;
swap(redstack, bluestack);
will be as efficient as possible.
Consider adding move semantics to your class — stack(stack&&) noexcept
, stack& operator=(stack&&) noexcept
, void push(T&&)
(or just void push(T)
at that point). I assume you left them out on purpose for this simple example.
Your copyList
is recursive. This could blow your stack if you're copying a very long list. You successfully eliminated the recursion from your destructor — why not eliminate it here too?
node* copyList(node* l)
Since this member function doesn't need the this
pointer for anything, it should be marked static
. And it wouldn't hurt to add const
to the node you don't intend to modify, just for a tiny bit of self-documentation:
static node *copyList(const node *l)
$endgroup$
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
1
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returningconst T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g.if (x < stk.peek())
orstd::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.
$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same aspush(const T&)
except that you'd sayhead = new node(std::move(object), head)
instead ofhead = new node(object, head)
.
$endgroup$
– Quuxplusone
Jan 20 at 20:22
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f211811%2fc-updated-stack-code%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
#include <stdexcept>
And either <utility>
or <algorithm>
, for std::swap
.
stack(int max) {
I very strongly recommend you make this constructor explicit
. Right now it's a non-explicit (implicit) conversion, which means you're permitting your users to convert integers to stacks implicitly:
void foo(stack<int> s);
void bar() {
foo(42); // compiles and calls foo with stack<int>(42)
}
The general rule (at least for user codebases) is "make everything explicit
unless you have some specific reason that it needs to be implicit." So you should make your node
constructor explicit
as well.
(I say "at least for user codebases" because the STL itself doesn't follow that principle — partly because the principle wasn't understood in 1998, and partly due to differences of opinion among the authors. "STL style" would be to make stack(int)
explicit because it's a one-argument constructor, but leave all zero- and two-argument constructors implicit. I recommend against doing that.)
int max = -1; // -1 so isFull() == false when default constructor used
This comment doesn't help me understand. I would understand INT_MAX
, but -1
just looks like it's breaking the class invariant enforced by the one-argument constructor:
if (max <= 0) throw std::out_of_range("stack size must be > 0");
Looking at these two lines in isolation, actually, I briefly wondered "wait, doesn't that always throw? You don't initialize max
before that if
, so it would have its default value, which is less than zero..." But then I realized that the name max
here is overloaded: the max
in the if
statement is testing the function parameter, not the data member.
To eliminate confusion about name overloading, I strongly recommend that you sigil each of your data members with a trailing underscore: max_
, size_
, head_
.
Then you don't have to write this->
when you access a member:
this->max = max;
can become just
max_ = max;
You don't need the disambiguating this->
, because there's no confusion anymore about which max
/max_
you mean.
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
Do you see the typo?
Write some unit tests for your code! In particular, now that you've spotted a bug, write a unit test that would have caught this bug and commit it as part of the bugfix. That's called a "regression test."
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
FYI, the copy-and-swap idiom can be written more concisely if you want to:
stack& operator=(stack const& rhs)
{
stack(rhs).swap(*this);
return *this;
}
const void pop() {
The const
here is useless; remove it. (I wonder if it was a mistake for something similar — void pop() const
? constexpr void pop()
? but neither of those makes sense either.)
T peek() {
OTOH, this method should be const:
T peek() const {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return std::as_const(head->data);
}
I threw in an as_const
just to illustrate complete paranoia. We don't know what type T
is, right? So when you construct the return value T
, which constructor do you want to call — T(T&)
or T(const T&)
? Write a unit test for a type T
where it makes a difference, and see what happens.
getSize()
, isFull()
, and isEmpty()
should all be const
methods as well.
void swap(stack& other) noexcept
Good! You also do the using std::swap;
dance correctly — although, since you're not swapping anything but ints and pointers, the dance is unnecessary in this case, but hey it's good practice.
You did forget, though, that if you want the using std::swap;
dance to work for anyone else who uses your stack
class, you'll need to provide that free ADL function swap
. So we add an inline friend:
friend void swap(stack& a, stack& b) noexcept {
a.swap(b);
}
Now
using std::swap;
swap(redstack, bluestack);
will be as efficient as possible.
Consider adding move semantics to your class — stack(stack&&) noexcept
, stack& operator=(stack&&) noexcept
, void push(T&&)
(or just void push(T)
at that point). I assume you left them out on purpose for this simple example.
Your copyList
is recursive. This could blow your stack if you're copying a very long list. You successfully eliminated the recursion from your destructor — why not eliminate it here too?
node* copyList(node* l)
Since this member function doesn't need the this
pointer for anything, it should be marked static
. And it wouldn't hurt to add const
to the node you don't intend to modify, just for a tiny bit of self-documentation:
static node *copyList(const node *l)
$endgroup$
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
1
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returningconst T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g.if (x < stk.peek())
orstd::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.
$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same aspush(const T&)
except that you'd sayhead = new node(std::move(object), head)
instead ofhead = new node(object, head)
.
$endgroup$
– Quuxplusone
Jan 20 at 20:22
add a comment |
$begingroup$
#include <stdexcept>
And either <utility>
or <algorithm>
, for std::swap
.
stack(int max) {
I very strongly recommend you make this constructor explicit
. Right now it's a non-explicit (implicit) conversion, which means you're permitting your users to convert integers to stacks implicitly:
void foo(stack<int> s);
void bar() {
foo(42); // compiles and calls foo with stack<int>(42)
}
The general rule (at least for user codebases) is "make everything explicit
unless you have some specific reason that it needs to be implicit." So you should make your node
constructor explicit
as well.
(I say "at least for user codebases" because the STL itself doesn't follow that principle — partly because the principle wasn't understood in 1998, and partly due to differences of opinion among the authors. "STL style" would be to make stack(int)
explicit because it's a one-argument constructor, but leave all zero- and two-argument constructors implicit. I recommend against doing that.)
int max = -1; // -1 so isFull() == false when default constructor used
This comment doesn't help me understand. I would understand INT_MAX
, but -1
just looks like it's breaking the class invariant enforced by the one-argument constructor:
if (max <= 0) throw std::out_of_range("stack size must be > 0");
Looking at these two lines in isolation, actually, I briefly wondered "wait, doesn't that always throw? You don't initialize max
before that if
, so it would have its default value, which is less than zero..." But then I realized that the name max
here is overloaded: the max
in the if
statement is testing the function parameter, not the data member.
To eliminate confusion about name overloading, I strongly recommend that you sigil each of your data members with a trailing underscore: max_
, size_
, head_
.
Then you don't have to write this->
when you access a member:
this->max = max;
can become just
max_ = max;
You don't need the disambiguating this->
, because there's no confusion anymore about which max
/max_
you mean.
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
Do you see the typo?
Write some unit tests for your code! In particular, now that you've spotted a bug, write a unit test that would have caught this bug and commit it as part of the bugfix. That's called a "regression test."
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
FYI, the copy-and-swap idiom can be written more concisely if you want to:
stack& operator=(stack const& rhs)
{
stack(rhs).swap(*this);
return *this;
}
const void pop() {
The const
here is useless; remove it. (I wonder if it was a mistake for something similar — void pop() const
? constexpr void pop()
? but neither of those makes sense either.)
T peek() {
OTOH, this method should be const:
T peek() const {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return std::as_const(head->data);
}
I threw in an as_const
just to illustrate complete paranoia. We don't know what type T
is, right? So when you construct the return value T
, which constructor do you want to call — T(T&)
or T(const T&)
? Write a unit test for a type T
where it makes a difference, and see what happens.
getSize()
, isFull()
, and isEmpty()
should all be const
methods as well.
void swap(stack& other) noexcept
Good! You also do the using std::swap;
dance correctly — although, since you're not swapping anything but ints and pointers, the dance is unnecessary in this case, but hey it's good practice.
You did forget, though, that if you want the using std::swap;
dance to work for anyone else who uses your stack
class, you'll need to provide that free ADL function swap
. So we add an inline friend:
friend void swap(stack& a, stack& b) noexcept {
a.swap(b);
}
Now
using std::swap;
swap(redstack, bluestack);
will be as efficient as possible.
Consider adding move semantics to your class — stack(stack&&) noexcept
, stack& operator=(stack&&) noexcept
, void push(T&&)
(or just void push(T)
at that point). I assume you left them out on purpose for this simple example.
Your copyList
is recursive. This could blow your stack if you're copying a very long list. You successfully eliminated the recursion from your destructor — why not eliminate it here too?
node* copyList(node* l)
Since this member function doesn't need the this
pointer for anything, it should be marked static
. And it wouldn't hurt to add const
to the node you don't intend to modify, just for a tiny bit of self-documentation:
static node *copyList(const node *l)
$endgroup$
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
1
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returningconst T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g.if (x < stk.peek())
orstd::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.
$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same aspush(const T&)
except that you'd sayhead = new node(std::move(object), head)
instead ofhead = new node(object, head)
.
$endgroup$
– Quuxplusone
Jan 20 at 20:22
add a comment |
$begingroup$
#include <stdexcept>
And either <utility>
or <algorithm>
, for std::swap
.
stack(int max) {
I very strongly recommend you make this constructor explicit
. Right now it's a non-explicit (implicit) conversion, which means you're permitting your users to convert integers to stacks implicitly:
void foo(stack<int> s);
void bar() {
foo(42); // compiles and calls foo with stack<int>(42)
}
The general rule (at least for user codebases) is "make everything explicit
unless you have some specific reason that it needs to be implicit." So you should make your node
constructor explicit
as well.
(I say "at least for user codebases" because the STL itself doesn't follow that principle — partly because the principle wasn't understood in 1998, and partly due to differences of opinion among the authors. "STL style" would be to make stack(int)
explicit because it's a one-argument constructor, but leave all zero- and two-argument constructors implicit. I recommend against doing that.)
int max = -1; // -1 so isFull() == false when default constructor used
This comment doesn't help me understand. I would understand INT_MAX
, but -1
just looks like it's breaking the class invariant enforced by the one-argument constructor:
if (max <= 0) throw std::out_of_range("stack size must be > 0");
Looking at these two lines in isolation, actually, I briefly wondered "wait, doesn't that always throw? You don't initialize max
before that if
, so it would have its default value, which is less than zero..." But then I realized that the name max
here is overloaded: the max
in the if
statement is testing the function parameter, not the data member.
To eliminate confusion about name overloading, I strongly recommend that you sigil each of your data members with a trailing underscore: max_
, size_
, head_
.
Then you don't have to write this->
when you access a member:
this->max = max;
can become just
max_ = max;
You don't need the disambiguating this->
, because there's no confusion anymore about which max
/max_
you mean.
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
Do you see the typo?
Write some unit tests for your code! In particular, now that you've spotted a bug, write a unit test that would have caught this bug and commit it as part of the bugfix. That's called a "regression test."
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
FYI, the copy-and-swap idiom can be written more concisely if you want to:
stack& operator=(stack const& rhs)
{
stack(rhs).swap(*this);
return *this;
}
const void pop() {
The const
here is useless; remove it. (I wonder if it was a mistake for something similar — void pop() const
? constexpr void pop()
? but neither of those makes sense either.)
T peek() {
OTOH, this method should be const:
T peek() const {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return std::as_const(head->data);
}
I threw in an as_const
just to illustrate complete paranoia. We don't know what type T
is, right? So when you construct the return value T
, which constructor do you want to call — T(T&)
or T(const T&)
? Write a unit test for a type T
where it makes a difference, and see what happens.
getSize()
, isFull()
, and isEmpty()
should all be const
methods as well.
void swap(stack& other) noexcept
Good! You also do the using std::swap;
dance correctly — although, since you're not swapping anything but ints and pointers, the dance is unnecessary in this case, but hey it's good practice.
You did forget, though, that if you want the using std::swap;
dance to work for anyone else who uses your stack
class, you'll need to provide that free ADL function swap
. So we add an inline friend:
friend void swap(stack& a, stack& b) noexcept {
a.swap(b);
}
Now
using std::swap;
swap(redstack, bluestack);
will be as efficient as possible.
Consider adding move semantics to your class — stack(stack&&) noexcept
, stack& operator=(stack&&) noexcept
, void push(T&&)
(or just void push(T)
at that point). I assume you left them out on purpose for this simple example.
Your copyList
is recursive. This could blow your stack if you're copying a very long list. You successfully eliminated the recursion from your destructor — why not eliminate it here too?
node* copyList(node* l)
Since this member function doesn't need the this
pointer for anything, it should be marked static
. And it wouldn't hurt to add const
to the node you don't intend to modify, just for a tiny bit of self-documentation:
static node *copyList(const node *l)
$endgroup$
#include <stdexcept>
And either <utility>
or <algorithm>
, for std::swap
.
stack(int max) {
I very strongly recommend you make this constructor explicit
. Right now it's a non-explicit (implicit) conversion, which means you're permitting your users to convert integers to stacks implicitly:
void foo(stack<int> s);
void bar() {
foo(42); // compiles and calls foo with stack<int>(42)
}
The general rule (at least for user codebases) is "make everything explicit
unless you have some specific reason that it needs to be implicit." So you should make your node
constructor explicit
as well.
(I say "at least for user codebases" because the STL itself doesn't follow that principle — partly because the principle wasn't understood in 1998, and partly due to differences of opinion among the authors. "STL style" would be to make stack(int)
explicit because it's a one-argument constructor, but leave all zero- and two-argument constructors implicit. I recommend against doing that.)
int max = -1; // -1 so isFull() == false when default constructor used
This comment doesn't help me understand. I would understand INT_MAX
, but -1
just looks like it's breaking the class invariant enforced by the one-argument constructor:
if (max <= 0) throw std::out_of_range("stack size must be > 0");
Looking at these two lines in isolation, actually, I briefly wondered "wait, doesn't that always throw? You don't initialize max
before that if
, so it would have its default value, which is less than zero..." But then I realized that the name max
here is overloaded: the max
in the if
statement is testing the function parameter, not the data member.
To eliminate confusion about name overloading, I strongly recommend that you sigil each of your data members with a trailing underscore: max_
, size_
, head_
.
Then you don't have to write this->
when you access a member:
this->max = max;
can become just
max_ = max;
You don't need the disambiguating this->
, because there's no confusion anymore about which max
/max_
you mean.
stack(stack const& rhs) :
head(copyList(rhs.head)),
size(rhs.size),
max(rhs.size) {}
Do you see the typo?
Write some unit tests for your code! In particular, now that you've spotted a bug, write a unit test that would have caught this bug and commit it as part of the bugfix. That's called a "regression test."
stack& operator = (stack const& rhs)
{
stack tmp(rhs);
swap(tmp);
return *this;
}
FYI, the copy-and-swap idiom can be written more concisely if you want to:
stack& operator=(stack const& rhs)
{
stack(rhs).swap(*this);
return *this;
}
const void pop() {
The const
here is useless; remove it. (I wonder if it was a mistake for something similar — void pop() const
? constexpr void pop()
? but neither of those makes sense either.)
T peek() {
OTOH, this method should be const:
T peek() const {
if (head == nullptr) throw std::underflow_error("cannot get item from empty stack");
return std::as_const(head->data);
}
I threw in an as_const
just to illustrate complete paranoia. We don't know what type T
is, right? So when you construct the return value T
, which constructor do you want to call — T(T&)
or T(const T&)
? Write a unit test for a type T
where it makes a difference, and see what happens.
getSize()
, isFull()
, and isEmpty()
should all be const
methods as well.
void swap(stack& other) noexcept
Good! You also do the using std::swap;
dance correctly — although, since you're not swapping anything but ints and pointers, the dance is unnecessary in this case, but hey it's good practice.
You did forget, though, that if you want the using std::swap;
dance to work for anyone else who uses your stack
class, you'll need to provide that free ADL function swap
. So we add an inline friend:
friend void swap(stack& a, stack& b) noexcept {
a.swap(b);
}
Now
using std::swap;
swap(redstack, bluestack);
will be as efficient as possible.
Consider adding move semantics to your class — stack(stack&&) noexcept
, stack& operator=(stack&&) noexcept
, void push(T&&)
(or just void push(T)
at that point). I assume you left them out on purpose for this simple example.
Your copyList
is recursive. This could blow your stack if you're copying a very long list. You successfully eliminated the recursion from your destructor — why not eliminate it here too?
node* copyList(node* l)
Since this member function doesn't need the this
pointer for anything, it should be marked static
. And it wouldn't hurt to add const
to the node you don't intend to modify, just for a tiny bit of self-documentation:
static node *copyList(const node *l)
answered Jan 19 at 16:53
QuuxplusoneQuuxplusone
12.2k12061
12.2k12061
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
1
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returningconst T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g.if (x < stk.peek())
orstd::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.
$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same aspush(const T&)
except that you'd sayhead = new node(std::move(object), head)
instead ofhead = new node(object, head)
.
$endgroup$
– Quuxplusone
Jan 20 at 20:22
add a comment |
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
1
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returningconst T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g.if (x < stk.peek())
orstd::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.
$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same aspush(const T&)
except that you'd sayhead = new node(std::move(object), head)
instead ofhead = new node(object, head)
.
$endgroup$
– Quuxplusone
Jan 20 at 20:22
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
$begingroup$
That was very helpful, thank you. Also for some reason the peek() method had the wrong return type. Shouldn’t it be const T& rather than T to prevent unnecessary copying of the object? This would be bad if T was large.
$endgroup$
– Samueljh1
Jan 19 at 22:12
1
1
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returning
const T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g. if (x < stk.peek())
or std::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
@Samueljh1: Yes, there is an efficiency advantage to returning
const T&
. There is also a disadvantage in that it opens the possibility of dangling-reference bugs. It depends on your use-case. If you're frequently going to do e.g. if (x < stk.peek())
or std::cout << x.peek()
— so returning by const ref would frequently save you a copy — then yeah, it's totally worth thinking about.$endgroup$
– Quuxplusone
Jan 19 at 23:04
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
Yes, I did think of that possibility, but the user should be responsible enough to not try to access the value after calling pop(). With the suggestion of move semantics you made, for example push(T&&), what exactly do I need to put in that method? Do i just call the regular push from there, or do I have to do something more complicated?
$endgroup$
– Samueljh1
Jan 20 at 18:10
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same as
push(const T&)
except that you'd say head = new node(std::move(object), head)
instead of head = new node(object, head)
.$endgroup$
– Quuxplusone
Jan 20 at 20:22
$begingroup$
That's a question for a different forum. Read all the answers to What are move semantics? at least. But basically it'd be the same as
push(const T&)
except that you'd say head = new node(std::move(object), head)
instead of head = new node(object, head)
.$endgroup$
– Quuxplusone
Jan 20 at 20:22
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f211811%2fc-updated-stack-code%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown