EXPLORATION 61

image

Exception-Safety

Exploration 45 introduced exceptions, which you have used in a number of programs since then. Dynamic memory presents a new wrinkle with regard to exceptions, and you must be that much more careful when handling them, in order to do so safely and properly in the face of dynamic memory management. In particular, you have to watch for memory leaks and similar problems.

Memory Leaks

Careless use of dynamic memory and exceptions can result in memory leaks—that is, memory that a program allocates but fails to free. In modern desktop operating systems, when an application terminates, the operating system reclaims all memory that the application used, so it is easy to become complacent about memory leaks. After all, no leak outlives the program invocation. But then your pesky users surprise you and leave your word processor (or whatever) running for days on end. They don’t notice the memory leaking until suddenly they can no longer edit documents, and the automatic backup utility cannot allocate enough memory to save the user’s work before the program terminates abruptly.

Maybe that’s an extreme example, but leaking memory is a symptom of mismanaging memory. If you mismanage memory in one part of the program, you probably mismanage memory in other parts too. Those other parts may be less benign than a mere memory leak.

Consider the silly program in Listing 61-1.

Listing 61-1.  Silly Program to Demonstrate Memory Leaks

#include <iostream>
#include <sstream>
#include <string>
 
int* read(std::istream& in)
{
  int value{};
  if (in >> value)
    return new int{value};
  else
    return nullptr;
}
 
int divide(int x, int y)
{
  return x / y;
}
 
int main()
{
  std::cout << "Enter pairs of numbers, and I will divide them. ";
  std::string line{};
  while(std::getline(std::cin, line))
  {
    std::istringstream input{line};
    if (int* x{read(input)})
      if (int* y{read(input)})
        std::cout << divide(*x, *y) << ' ';
  }
}

This program introduces a new C++ feature. The if statements in main() define variables inside their conditionals. The rules for this feature are restrictive, so it is not used often. You can define only one declarator, and you must initialize it. The value is then converted to bool, which in this case means comparing to a null pointer. In other words, this conditional is true if the pointer is not null. The scope of the variable is limited to the body of the conditional (including the else portion of an if statement).

Now that you can understand it, what’s wrong with this program?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The program leaks memory. It leaks memory if a line of text contains only one number. It also leaks memory if a line of text contains two numbers. In short, the program leaks like a termite’s rowboat. Adding delete expressions should fix things, right? Do it. Your program may now look like Listing 61-2.

Listing 61-2.  Adding delete Expressions to the Silly Program

#include <iostream>
#include <sstream>
#include <string>
 
int* read(std::istream& in)
{
  int value{};
  if (in >> value)
    return new int{value};
  else
    return nullptr;
}
 
int divide(int x, int y)
{
  return x / y;
}
 
int main()
{
  std::cout << "Enter pairs of numbers, and I will divide them. ";
  std::string line{};
  while(std::getline(std::cin, line))
  {
    std::istringstream input{line};
    if (int* x{read(input)})
    {
      if (int* y{read(input)})
      {
        std::cout << divide(*x, *y) << ' ';
        delete y;
      }
      delete x;
    }
  }
}

Well, that’s a little better, but only a little. Let’s make the problem more interesting by adding some exceptions.

Exceptions and Dynamic Memory

Exceptions can be a significant factor for memory errors. You may write a function that carefully matches every new with a corresponding delete, but an exception thrown in the middle of the function will cause that oh-so-carefully-written function to fail, and the program forgets all about that dynamically allocated memory.

Anytime you use a new expression, you must be aware of places in your program that may throw an exception. You must have a plan for how to manage the exception, to ensure that you don’t lose track of the dynamically allocated memory and that the pointer always holds a valid address. Many places can throw exceptions, including new expressions, any I/O statement (if the appropriate exception mask bit is set, as explained in Exploration 45), and a number of other library calls.

To see an example of how exceptions can cause problems, read the program in Listing 61-3.

Listing 61-3.  Demonstrating Issues with Exceptions and Dynamic Memory

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
 
int* read(std::istream& in)
{
  int value{};
  if (in >> value)
    return new int{value};
  else
    return nullptr;
}
 
int divide(int x, int y)
{
  if (y == 0)
    throw std::runtime_error{"integer divide by zero"};
  else if (x < y)
    throw std::underflow_error{"result is less than 1"};
  else
    return x / y;
}
 
int main()
{
  std::cout << "Enter pairs of numbers, and I will divide them. ";
  std::string line{};
  while(std::getline(std::cin, line))
    try
    {
      std::istringstream input{line};
      if (int* x{read(input)})
      {
        if (int* y{read(input)})
        {
          std::cout << divide(*x, *y) << ' ';
          delete y;
        }
        delete x;
      }
    } catch (std::exception const& ex) {
      std::cout << ex.what() << ' ';
    }
}

Now what’s wrong with this program?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The program leaks memory when the divide function throws an exception. In this case, the problem is easy to see, but in a more complicated program, it can be harder to identify. Looking at the input loop, it seems that every allocation is properly paired with a delete expression. But in a more complicated program, the source of exceptions and the try-catch statement may be far apart and unrelated to the input loop.

Ideally, you should be able to manage memory without knowing about exceptions. Fortunately, you can—at least to a certain degree.

Automatically Deleting Pointers

Keeping track of allocated memory can be tricky, so you should accept any help that C++ can offer. One class template that can help a lot is std::unique_ptr<> (defined in the <memory> header). This template wraps a pointer, so that when the unique_ptr object goes out of scope, it automatically deletes the pointer it wraps. The template also guarantees that exactly one unique_ptr object owns a particular pointer. Thus, when you assign one unique_ptr to another, you know exactly which unique_ptr (the target of the assignment) owns the pointer and has responsibility for freeing it. You can assign unique_ptr objects, pass them to functions, and return them from functions. In all cases, ownership passes from one unique_ptr object to another. Like children playing the game of Hot Potato, whoever is left holding the pointer or potato in the end is the loser and must delete the pointer.

The unique_ptr template is particularly helpful when a program throws an exception. When C++ handles the exception, it unwinds the stack and destroys local variables along the way. This means it will destroy local unique_ptr objects in those unwound stack frames, which will delete their pointers. Without unique_ptr, you may get a memory leak.

Thus, a common idiom is to use unique_ptr<> for local variables of pointer type and for data members of pointer type. Equally viable, but less common, is for function parameters and return types to be unique_ptr. Return a unique_ptr normally, but a function parameter must be declared as an rvalue reference, and the caller must move the pointer to the function, as illustrated in Listing 61-4.

Listing 61-4.  Using the unique_ptr Class Template

#include <iostream>
#include <memory>
 
class see_me
{
public:
  see_me(int x) : x_{x} { std::cout <<  "see_me(" << x_ << ") "; }
  ~see_me()             { std::cout << "~see_me(" << x_ << ") "; }
  int value() const     { return x_; }
private:
  int x_;
};
 
std::unique_ptr<see_me> nothing(std::unique_ptr<see_me>&& arg)
{
  return std::move(arg);
}
 
template<class T>
std::unique_ptr<T> make(int x)
{
  return std::unique_ptr<T>{new T{x}};
}
 
int main()
{
  std::cout << "program begin . . . ";
  std::unique_ptr<see_me> sm{make<see_me>(42)};
  std::unique_ptr<see_me> other;
  other = nothing(std::move(sm));
  if (sm.get() == nullptr)
    std::cout << "sm is null, having lost ownership of its pointer ";
  if (other.get() != nullptr)
    std::cout << "other now has ownership of the int, " <<
                 other->value() << ' ';
  std::cout << "program ends . . . ";
}

As you can see, the get() member function returns the raw pointer value, which you can use to test the pointer or pass to functions that do not expect to gain ownership of the pointer. You can assign a new unique_ptr value, which causes the target of the assignment to delete its old value and take ownership of the new pointer. Dereference the unique_ptr pointer with *, or use -> to access members, the same way you would with an ordinary pointer.

Use std::unique_ptr to fix the program in Listing 61-3. Compare your repairs with mine, which are presented in Listing 61-5.

Listing 61-5.  Fixing Memory Leaks

#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
 
std::unique_ptr<int> read(std::istream& in)
{
  int value;
  if (in >> value)
    return std::unique_ptr<int>{new int{value}};
  else
    return std::unique_ptr<int>{};
}
 
int divide(int x, int y)
{
  if (y == 0)
    throw std::runtime_error("integer divide by zero");
  else if (x < y)
    throw std::underflow_error("result is less than 1");
  else
    return x / y;
}
 
int main()
{
  std::cout << "Enter pairs of numbers, and I will divide them. ";
  std::string line{};
  while(std::getline(std::cin, line))
    try
    {
      std::istringstream input{line};
      if (std::unique_ptr<int> x{read(input)})
      {
        if (std::unique_ptr<int> y{read(input)})
          std::cout << divide(*x, *y) << ' ';
      }
    } catch (std::exception const& ex) {
      std::cout << ex.what() << ' ';
    }
}

The changes are minimal, but they vastly increase the safety of this program. No matter what happens in the divide function or elsewhere, this program does not leak any memory. You will learn more about unique_ptr in Exploration 63.

Exceptions and Constructors

Even without unique_ptr, C++ guarantees one level of exception-safety when constructing an object: if a constructor throws an exception, the compiler automatically cleans up base-class portions of the incompletely constructed object. If some data members have been initialized successfully, they are destroyed in reverse order of creation, but uninitialized data members are left alone. The new expression never completes, so it never returns a pointer to an incomplete object. But a raw pointer data member has no destructor. The compiler does not automatically delete the memory, so any memory allocated for pointer-type data members will be stranded. Listing 61-6 demonstrates how constructors and exceptions interact.

Listing 61-6.  Demonstrating Constructors That Throw Exceptions

#include <iostream>
 
class see_me
{
public:
  see_me(int x) : x_{x} { std::cout <<  "see_me(" << x_ << ") "; }
  ~see_me()             { std::cout << "~see_me(" << x_ << ") "; }
private:
  int x_;
};
 
class bomb : public see_me
{
public:
  bomb() : see_me{1}, a_{new see_me{2}} { throw 0; }
  ~bomb() {
    delete a_;
  }
private:
  see_me *a_;
};
 
int main()
{
  bomb *b{nullptr};
  try {
    b = new bomb;
  } catch(int) {
    if (b == nullptr)
      std::cout << "b is null ";
  }
}

Predict the output from this program:

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Run the program. What is the actual output?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Explain your observations.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The bomb class throws an exception in its constructor. It derives from see_me and has an additional member of type pointer to see_me. The see_me class lets you see the constructors and destructors, so you can see that the data member, see_me(2), is constructed, but never destroyed, which indicates a memory leak. The bomb destructor never runs, because the bomb constructor never finishes. Therefore, a_ is never cleaned up. On the other hand, see_me(1) is cleaned up because the base class is automatically cleaned up if a derived-class constructor throws an exception.

The main() function catches the exception and recognizes that the b variable was never assigned. Thus, there’s nothing for the main() function to clean up.

What happens if you were to use unique_ptr in the bomb class? Try and see. What happens?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Listing 61-7 shows the new program.

Listing 61-7.  Using unique_ptr in bomb

#include <iostream>
#include <memory>
 
class see_me
{
public:
  see_me(int x) : x_{x} { std::cout <<  "see_me(" << x_ << ") "; }
  ~see_me()             { std::cout << "~see_me(" << x_ << ") "; }
private:
  int x_;
};
 
class bomb : public see_me
{
public:
  bomb() : see_me{1}, a_{new see_me{2}} { throw 0; }
  ~bomb() {}
private:
  std::unique_ptr<see_me> a_;
};
 
int main()
{
  bomb *b{nullptr};
  try {
    b = new bomb;
  } catch(int) {
    if (b == nullptr)
      std::cout << "b is null ";
  }
}

Notice that all the see_me objects are now properly destroyed. Even though the constructor does not finish before throwing an exception, any data members that have been constructed will be destroyed. Thus, unique_ptr objects are cleaned up. Ta da! Mission accomplished!

Or is it? Even with unique_ptr, you must still be cautious. Consider the program in Listing 61-8.

Listing 61-8.  Mystery Program That Uses unique_ptr

#include <iostream>
#include <memory>
 
class mystery
{
public:
  mystery() {}
  mystery(mystery const&) { throw "oops"; }
};
 
class demo
{
public:
  demo(int* x, mystery m, int* y) : x_{x}, m_{m}, y_{y} {}
  demo(demo const&) = delete;
  demo& operator=(demo const&) = delete;
  int x() const { return *x_; }
  int y() const { return *y_; }
private:
  std::unique_ptr<int> x_;
  mystery            m_;
  std::unique_ptr<int> y_;
};
 
int main()
{
  demo d{new int{42}, mystery{}, new int{24}};
  std::cout << d.x() << d.y() << ' ';
}

What’s wrong with this program?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

To help you understand what the program does, use a see_me object instead of int. Does that help you understand?

The demo class uses unique_ptr to ensure proper lifetime management of its pointers. It deletes its copy constructor and copy assignment operator to avoid any problems they may cause.

The problem is the basic design of the demo constructor. By taking two pointer arguments, it opens the possibility of losing track of these pointers before it can safely tuck them away in their unique_ptr wrappers. The mystery class forces an exception, but in a real program, unexpected exceptions can arise from a variety of less explicit sources.

The simplest solution is to force the caller to use unique_ptr by changing the demo constructor, as demonstrated in the following:

demo(std::unique_ptr<int>&& x, mystery m, std::unique_ptr<int>&& y)
: x_{std::move(x)}, m_{m}, y_{std::move(y)}
{}

The unique_ptr class would have simplified much of the messiness of dealing with pointers back in Exploration 59, but if you learned about unique_ptr first, you wouldn’t be able to fully appreciate all that it does for you. It has even more magic to help you, and Exploration 63 will take a closer look at unique_ptr and some of its friends. But first, let’s take a side trip and discover the close connection between old-fashioned, C-style arrays and pointers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset