C++ Updated Stack Code












1












$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









share|improve this question











$endgroup$

















    1












    $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









    share|improve this question











    $endgroup$















      1












      1








      1





      $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









      share|improve this question











      $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






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Jan 19 at 17:41









      Jamal

      30.3k11116227




      30.3k11116227










      asked Jan 19 at 14:52









      Samueljh1Samueljh1

      355




      355






















          1 Answer
          1






          active

          oldest

          votes


















          7












          $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)





          share|improve this answer









          $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 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$
            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











          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
          });


          }
          });














          draft saved

          draft discarded


















          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









          7












          $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)





          share|improve this answer









          $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 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$
            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
















          7












          $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)





          share|improve this answer









          $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 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$
            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














          7












          7








          7





          $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)





          share|improve this answer









          $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)






          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered Jan 19 at 16:53









          QuuxplusoneQuuxplusone

          12.1k12061




          12.1k12061












          • $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 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$
            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 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 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$
            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 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


















          draft saved

          draft discarded




















































          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.




          draft saved


          draft discarded














          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





















































          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







          Popular posts from this blog

          Biblatex bibliography style without URLs when DOI exists (in Overleaf with Zotero bibliography)

          ComboBox Display Member on multiple fields

          Is it possible to collect Nectar points via Trainline?