Writing a Safer Constructor

Constructor is the most basic and important concept in C++.

Take a look the basic example here, and try finding the problems.

The header file: DebugOnly.h for logging debug message

#ifndef DebugOnly_h_
#define DebugOnly_h_

#include <iostream>
#include <sstream>        // std::stringstream
#include <string>         // std::string

#define DEBUG     true

void Log(std::string msg)
{
  DEBUG && std::cout << msg << std::endl;
}

template<class T>
std::string ToAddressString(const T& t)
{
  const void* address = static_cast<const void*>(&t);
  std::stringstream ss;
  ss << address;
  return ss.str();
}

#endif // #ifndef DebugOnly_h_

The core part for our demo:

#include <iostream>
#include "DebugOnly.h"

using std::string;

struct Person
{
  string name;
  unsigned int age;

  Person(string name, int age)
    : name(name)
    , age(age)
  {
  }
};

class Book
{
public:
  Book(string title, int price, Person* author)
    : title(title)
    , price(price)
    , author(author)
  {
  }

  string Title()
  {
    return title;
  }

  unsigned int Price()
  {
    return price;
  }

  Person& Author()
  {
    return *author;
  }

  ~Book()
  {
    Log("~Book: " + title);
    // Release it to avoid memory leak
    delete  author;
  }

private:
  string        title;
  unsigned int  price;
  Person*       author;
};

//   Helper Functions
// ===================================
void LogBook(Book& book)
{
  Log("[" + ToAddressString(book) + "] " +    // memory address
      book.Title() + ", " +                   // book's title
      std::to_string(book.Price()) + ", " +   // book's price
      book.Author().name + ", " +             // author's name
      std::to_string(book.Author().age));     // author's age
}

void LogPerson(Person& person)
{
  Log("[" + ToAddressString(person) + "] " +  // momery address
      person.name + " " +                     // name
      std::to_string(person.age));            // age
}

//   Main Program
// ===================================
int main()
{
  Log("----- book 1 -----");
  {
    Person* ChunMin = new Person("ChunMin", 20);
    LogPerson(*ChunMin);
    Book book1 = Book("C++ Notes", 179, ChunMin);
    LogBook(book1);
  }

  Log("----- book 2 -----");
  Person Jimi("Jimi", 18);
  LogPerson(Jimi);

  {
    Book book2 = Book("Python Notes", 79, &Jimi);
    LogBook(book2);
  }
}

Do you find the problems in the above example? If you already find it, cheers! You might not need to read the remaining parts. If you don't, don't worry, I have a good news to you: You will learn something in this post!

The Gone Jimi

Let's see the executed result of the above example:

----- book 1 -----
[0x7fb402404a90] ChunMin 20
[0x7fff594cb8a8] C++ Notes, 179, ChunMin, 20
~Book: C++ Notes
----- book 2 -----
[0x7fff594cb858] Jimi 18
[0x7fff594cb818] Python Notes, 79, Jimi, 18
~Book: Python Notes
a.out(2100,0x7fff7293d300) malloc: *** error for object 0x7fff594cb858: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

The error is occured at address 0x7fff594cb858. The problem is that: There is no allocated object there to be freed! Why? We can easily prove that there is a Person object whose name is Jimi allocated at address 0x7fff594cb858 from the log. Why my computer will tell me that the Jimi is gone?

The truth is that: The Jimi object is actually freed twice!

The lifetime of the variable is depended on its containing scope('{' and '}'). The book1 will be freed once the program runs beyond the '}' after LogBook(book1);. You can see that the book1's deconstructor is called between LogBook(book1); and Log("----- book 2 -----");.

Similarly, the book2's deconstructor will be called upon the program runs beyond the '}' after LogBook(book2);. At that time, the memory pointed by the book2.author, which is exactly the Jimi object, will be freed. That is the first time the Jimi object will be released. Next, after the program runs beyond the final '}', the Jimi object will be freed again because the program already exceeds its containing scope(the '{' and '}' of main). That is the second time. However, one memory space can't be freed twice, and that's why we get an error there.

Leave Jimi alone

How to avoid that kind of problems? It's pretty easy. The answer is that we have to clone the Jimi in book2. That is, we have to clone our own Person object, author, in the Book class. Then, we don't need to worry about we'll kill the wrong guy in the Book's deconstructor anymore. We will always kill our own copied author data in the Book's deconstructor.

Thus, we should write a safer constructor like:

#include "DebugOnly.h"

using std::string;

struct Person
{
  string name;
  unsigned int age;

  Person(string name, int age)
    : name(name)
    , age(age)
  {
  }
};

class Book
{
public:
  Book(string title, int price, Person& author) // Using reference instead of pointer
    : title(title)
    , price(price)
    , author(new Person(author)) // Create its own data here!
  {
  }

  string Title()
  {
    return title;
  }

  unsigned int Price()
  {
    return price;
  }

  Person& Author()
  {
    return *author;
  }

  ~Book()
  {
    Log("~Book: " + title);
    // Release it to avoid memory leak
    delete  author;
  }

private:
  string        title;
  unsigned int  price;
  Person*       author;
};

//   Helper Functions
// ===================================
void LogBook(Book& book)
{
  Log("[" + ToAddressString(book) + "] " +    // memory address
      book.Title() + ", " +                   // book's title
      std::to_string(book.Price()) + ", " +   // book's price
      book.Author().name + ", " +             // author's name
      std::to_string(book.Author().age));     // author's age
}

void LogPerson(Person& person)
{
  Log("[" + ToAddressString(person) + "] " +  // momery address
      person.name + " " +                     // name
      std::to_string(person.age));            // age
}

//   Main Program
// ===================================
int main()
{
  Log("----- book 1 -----");
  {
    Person* ChunMin = new Person("ChunMin", 20);
    LogPerson(*ChunMin);
    Book book1 = Book("C++ Notes", 179, *ChunMin); // Using reference instead of pointer
    LogBook(book1);
  }

  Log("----- book 2 -----");
  Person Jimi("Jimi", 18);
  LogPerson(Jimi);

  {
    Book book2 = Book("Python Notes", 79, Jimi); // Using reference instead of pointer
    LogBook(book2);
  }
}

And here is the executed result, without error:

----- book 1 -----
[0x7fdd9a404a90] ChunMin 20
[0x7fff59c6f8a8] C++ Notes, 179, ChunMin, 20
~Book: C++ Notes
----- book 2 -----
[0x7fff59c6f858] Jimi 18
[0x7fff59c6f818] Python Notes, 79, Jimi, 18
~Book: Python Notes

Actually, we might directly use a Person object as our member variable more often than a pointer to Person object.

The new operator at author(new Person(author)) in the second example will request a memory chunk as big as the Person object needed from heap, and then the allocated memory will be initialized by Person's constructor with the input author's data(the auther in Person(author)). Finally, the address will be set to the Book.author (the auther outside of the (new Person(...))).

If there're lots of Book objects, we will request lots of memory chunks from heap. It will lead the memory fragment problem (please refer our memory fragment to get more detail), especially when we don't know when the Book objects will be released.

Therefore, the less new operators are used, the less fragment are generated in memory.

Don't let Jimi mess about in memory

struct Person
{
  string name;
  unsigned int age;

  Person(string name, int age)
    : name(name)
    , age(age)
  {
  }
};

class Book
{
public:
  Book(string title, int price, Person& author) // Using reference instead of pointer
    : title(title)
    , price(price)
    , author(author) // Create its own data by copy!
  {
  }

  string Title()
  {
    return title;
  }

  unsigned int Price()
  {
    return price;
  }

  Person& Author()
  {
    return author;
  }

  ~Book()
  {
    Log("~Book: " + title);
  }

private:
  string        title;
  unsigned int  price;
  Person        author;
};

It's not only more intuitive, but also can avoid the memory fragment problem when there're lots of Book objects.

results matching ""

    No results matching ""