How to protect user if can't return nullptr for an empty stack?

Viewed 145

I am trying to code the implementation of the Cube, Stack example given in this Coursera course example of Towers of Hanoi to learn more C++.

In stack.h I have to implement:

class Stack {
public:
void push_back(const Cube & cube);
Cube removeTop();
Cube & peekTop();
unsigned size() const;

friend std::ostream & operator<<(std::ostream & os, const Stack & stack);

private:
std::vector<Cube> cubes_;
};

The issue I have is with removeTop(). I was thinking of returning nullptr if the stack (vector) is empty because pop_back's behavior is undefined for an empty vector.

Calling pop_back on an empty container is undefined. Cpp Reference

inline Cube Stack::removeTop() {
  if (!cubes_.empty()) {
    Cube top_cube = cubes_.back();
    cubes_.pop_back();
    return top_cube;
  }
  else {
    return nullptr;
  }
}

However, I get an error during compilation.

./stack.h:35:12: error: no viable conversion from returned value of type
      'std::__1::nullptr_t' to function return type 'uiuc::Cube'
    return nullptr;

How can I protect the user if I can't return a nullptr? Am I limited to just telling the user that the function should not be called on an empty stack and let him/her take care of the checking?

4 Answers

This is exactly what exceptions are for:

if (cubes_.empty())
    throw std::runtime_error("stack underflow");
Cube top_cube = cubes_.back();
cubes_.pop_back();
return top_cube;

Complicating this with std::optional is almost certainly not the right answer here. Attempting to pop from an empty stack means the program has lost its way. That should be a hard error, not something that's masked by an interface that says "you might or might not have this, please check afterwards".

Maybe like this:

inline bool Stack::removeTop(Cube& top_cube) {
  if (!cubes_.empty()) {
    top_cube = cubes_.back();
    cubes_.pop_back();
    return true;
  }
  else {
    return false;
  }
}

Based on the function signature, you have to implement, you really can't. Typically, you'd guard this sort of thing with an assertion. In some situations you might use the NullObject pattern, or you could return a junk object. In newer C++ versions, you can also use std::optional<T>.

inline Cube Stack::removeTop() {
  if (!cubes_.empty()) {
    Cube top_cube = cubes_.back();
    cubes_.pop_back();
    return top_cube;
  }
  else {
    return Cube {};
  }
}

As pyj noted, since c++17 there is a new mechanism to do so (adopting boost::optional idea). This is the std::optional container. When using std::optional, you tell the user of this function, that he might get an empty response, and therefore, he must check whether it is initialized or not.

From the cpp reference:

The class template std::optional manages an optional contained value, i.e. a value that may or may not be present.

A common use case for optional is the return value of a function that may fail. As opposed to other approaches, such as std::pair, optional handles expensive-to-construct objects well and is more readable, as the intent is expressed explicitly.

Now for the usage:

inline std::optional<Cube> Stack::removeTop() 
{
    if (!cubes_.empty()) 
    {
        Cube top_cube = cubes_.back();
        cubes_.pop_back();
        return top_cube;
    }
    else 
    {
        return std::nullopt;
    }
}

And, while you get a std::optional, and not a Cube, there is no memory allocation on the heap, which is a great advantage over using pointers for the same output.

If an optional contains a value, the value is guaranteed to be allocated as part of the optional object footprint, i.e. no dynamic memory allocation ever takes place. Thus, an optional object models an object, not a pointer, even though operator*() and operator->() are defined.

Related