Chapter 7. Controlling Object Lifetimes

After completing this chapter, you will be able to:

  • Describe how Microsoft .NET memory management differs from traditional C++ memory management.

  • Provide finalizers and destructors for your classes.

  • Create objects by using stack semantics.

Now that you know how to create objects in C++/CLI by using the gcnew operator, it’s time to learn how to control object lifetimes as well as another way to create and use objects.

The .NET approach to object lifetimes

We’ve seen what happens at the start of an object’s life, but what happens when an object is no longer required?

There are two things that need to happen when an object comes to the end of its life:

  • You might want to do some clean-up before the object is destroyed, such as writing data back to a database

  • The object’s memory needs to be reclaimed by the runtime

Let’s see how this is done in C++/CLI. In .NET, like Java and many other modern languages, the runtime is responsible for ensuring that memory from dead objects is reclaimed. The component that does this is called the garbage collector. The runtime keeps track of handles to objects, and when an object can no longer be referenced through any handle, it is unreachable and is a candidate for garbage collection.

This means that programmers need to keep several things in mind:

  • Objects are always used through handles, because that’s the way that the system keeps track of them.

  • An object will always be available as long as there is at least one handle to it.

  • You cannot tell when an object’s memory will be reclaimed; this is up to the garbage collector.

Destruction and finalization

Before we can start looking at code, let’s introduce two new terms. Finalization is what happens when an object’s memory is about to be reclaimed and is under the control of the garbage collector. You can provide code to be executed at this point, in the form of a finalizer method on your class.

But it might be that you know definitely at some point in the code that you no longer need the object, and you would like it to tidy itself up there and then. For example, if you are working with a Customer object, you might want the object to save its data back to the database when you’ve finished with it. This is called destruction, and you can provide a destructor method in your class.

With C++/CLI, you can provide code to be executed at both these points in an object’s lifecycle, as you will see in the following sections.

Destructors

A destructor is executed when you no longer need an object. To provide a destructor for a class, add a member function that has the same name as the class but is preceded by a tilde character (~).

ref class MyClass
{
public:
    MyClass();      // constructor
    ~MyClass();     // destructor
};

You can signal that you no longer need an object by calling delete on a handle to the object.

// Create an Account
Account ^acc = gcnew Account();

// Use the Account

// We no longer need the Account
delete acc;

At this point in the code the destructor is called; thus, you know exactly where and when the object has ceased to operate.

Here are three points you should note about destructors:

  • Like the constructor, they have no return type, and it is an error to give them one.

  • They do not take any arguments, which means they cannot be overloaded.

  • Destructors are usually public members of a class. If you make them private, you might not be able to destroy objects of that type.

Finalizers

Finalizers are called when the garbage collector finally reclaims the object’s memory. You will need a finalizer if you have unmanaged resources, such as pointers to unmanaged classes, file handles, window handles, graphic device contexts, and so on. If you don’t have any of those—and you’ll only tend to do that when you are working with unmanaged code—you probably don’t need a finalizer.

A finalizer is a member function that has the same name as the class but is preceded by an exclamation mark (!).

ref class MyClass
{
public:
    MyClass();      // constructor
    !MyClass();     // finalizer
};

You can see that finalizers obey the same rules as destructors; they have the same name as the class and don’t have a return type or take arguments.

A few points about finalizers

There are three things that you should be aware of when using finalizers.

First, don’t define a finalizer for your class if you don’t have anything for it to do. In most cases, adding an empty function to a class will have little effect, but that isn’t the case for finalizers. If the garbage collector sees that your class implements a finalizer, it knows that it has to run this before reclaiming objects of that type, and this slows down the collection process.

Second, no guarantee is made as to the order in which finalizers will run, which can be problematic if objects have dependencies on one another. Suppose that two objects, A and B, both have a finalizer, and that both of them update a data resource. Both finalizers will be called when the objects are destroyed, but you can’t know which one will be called first. This means that you can’t determine in what order data will be written to the data resource, which could cause a problem.

And third, finalizers aren’t called during application termination for objects that are still live, such as those being used by background threads or those created during the execution of a finalizer. Although all system resources will be freed up when the application exits, objects that don’t have their finalizers called might not get a chance to clean up properly.

This might give you the impression that finalizers should be avoided. Although they are useful in some situations, you will find that you can normally do whatever cleanup you require in the destructor.

Implementing the destructor and finalizer for a class

In this exercise, you will see how to create and use the finalizer and destructor for a class.

  1. Start Visual Studio 2012 and create a new CLR Console Application called Lifetimes.

  2. On the Project menu, click Add New Item.

  3. In the Add New Item dialog box, in the pane on the left, select Visual C++, and then, in the center pane, click Header File (.h).

  4. Toward the bottom of the dialog box, in the Name field, type MyClass.h, and then click Add.

    Note

    Another way that you can open the Add New Item dialog box is to right-click the project name in Solution Explorer and then, in the shortcut menu that appears, point to Add, and then click New Item.

  5. Open the header file and add the declaration for a class that has a constructor, a destructor, a finalizer, and a single work method, which you will call to show that the object can be used.

    using namespace System;
    
    ref class MyClass
    {
        String ^name;
    public:
        MyClass(String ^objectName);    // constructor
        ~MyClass();                     // destructor
        !MyClass();                     // finalizer
        void DoSomething();             // 'work' method
    };
  6. Repeat steps 2 through 4, but this time add a source file called MyClass.cpp to the project. Open the file and add #include statements for stdafx.h and MyClass.h.

    #include "stdafx.h"
    using namespace std;
    
    #include "MyClass.h"
  7. Implement the constructor so that it stores the name in the data member and prints a message to show that has been called.

    MyClass::MyClass(String ^objectName)
    {
        name = objectName;
        Console::WriteLine("Constructor called for {0}", name);
    }

    Note

    Up to this point, you have used multiple Write and WriteLine statements to build up a line of output. This exercise introduces a more efficient way: call WriteLine or Write with a string that contains text and markers that consist of a number in braces, such as {0} and {1}. The string should be followed by a list of items that you want to print out. The first item will be output in place of {0}, the second in place of {1}, and so on. We will use this from now on to save typing (and paper!).

  8. Implement the destructor to print a message to show that has been called.

    MyClass::~MyClass()
    {
        Console::WriteLine("Destructor called for {0}", name);
    }
  9. Implement the finalizer to print a message to show that it has been called.

    MyClass::!MyClass()
    {
        Console::WriteLine("Finalizer called for {0}", name);
    }
  10. Implement the DoSomething method to print out a message. This is to show that the object has been used between creation and destruction.

    void MyClass::DoSomething()
    {
        Console::WriteLine("DoSomething called for {0}", name);
    }
  11. Build the project and fix any compiler errors.

Using the finalizer

In this exercise you will see how the finalizer for a class is called.

  1. Continue using the project from the previous exercise.

  2. Open Lifetimes.cpp and in the main method of the application, create an object by using gcnew, and then call DoSomething. Remember to add a #include for MyClass.h, as shown here:

    #include "MyClass.h"
    
    int main(array<System::String^>^ args)
    {
        MyClass ^m1 = gcnew MyClass("m1");
        m1->DoSomething();
    
        Console::WriteLine();
        Console::WriteLine("End of program");
        Console::WriteLine();
        return 0;
    }
  3. Build and run the application.

    Output similar to the following appears:

    Constructor for m1
    DoSomething called
    
    End of Program
    
    Finalizer called for m1

When you create an object, its constructor is called. If the application finishes and the object hasn’t been destroyed, the garbage collector will call the finalizer to clear up any unmanaged resources associated with the object.

Using the destructor

In this exercise you will see how the destructor for a class is called.

  1. Continue using the project from the previous exercise.

  2. Edit the code so that you explicitly delete the object after using it. Do this by inserting a call to delete after the call to DoSomething.

    MyClass ^m1 = gcnew MyClass("m1");
    m1->DoSomething();
    delete m1;
  3. Build and run the application.

    Output similar to the following appears:

    Constructor called for m1
    DoSomething called for m1
    Destructor called for m1
    
    End of Program

Notice that two things have happened: first, the destructor has been called at the point where you called delete; second, the finalizer was not called at the end of the application.

The destructor being called when you call delete means that you have complete control over when objects tidy themselves up. This deterministic destruction is a hallmark of traditional C++, and it is the basis of many common C++ coding idioms. You should make a habit of calling delete on your object handles when you no longer need them.

We also saw that as a result of calling delete, the finalizer wasn’t executed. The garbage collector decides that you have dealt with the disposal of an object if its destructor has been executed, and so it doesn’t need to execute its finalizer. This means that if you do have a finalizer, you should call it from the destructor to ensure that all unmanaged resources are freed up no matter how your objects exit.

MyClass::~MyClass()
{
    // Free up managed resources: this will be done anyway by the runtime
    // Now call the finalizer to free unmanaged resources
    this->!MyClass()
}

Objects and stack semantics

It might seem rather tedious to have to create objects by using gcnew and then call delete on the handles when you have finished using them. After all, wasn’t the idea of garbage collection supposed to be that you didn’t have to keep track of your objects when you finished with them?

It is important not to confuse the concept of an object tidying up after itself with the runtime reclaiming the object’s memory; the two are independent of one another. You might want to say what an object does to tidy up when you have finished with it but not really care when the garbage collector decides to reclaim its memory. In this case, you would implement a destructor, which you can then call by using delete.

Traditional C++ object creation and destruction

Traditional C++ objects can also have destructors and be dynamically created and destroyed in a manner very similar to the one to which you have become accustomed. They use new instead of gcnew, but the mechanism is very similar.

There is, however, another way by which objects can be created in standard C++, and that is to create them on the stack as local objects, such as illustrated in the following:

MyClass m("m3");
m.DoSomething();

You can see two differences from C++/CLI code here. The more obvious of them is that you don’t use gcnew and you don’t create a handle. This syntax creates an object called m, and the constructor parameters are passed after the object name in the same way as they were passed to gcnew when creating an object dynamically. The second obvious difference is that members of the object are accessed by using the dot operator (.) rather than ->.

There is one important consequence to creating objects in this way, apart from it taking slightly less typing. When you create an object in this manner, its destructor is called automatically at the end of the block of code. This is shown in the following code sample:

{
    MyClass m("m3");
    m.DoSomething();
}  // Destructor for m is called here

Such objects are sometimes called automatic objects because they are automatically destroyed when they go out of scope.

Note

In C++, scope refers to where in the code an object is visible. It is often related to an object’s lifetime. In this case, m cannot be seen outside the block, so it goes out of scope at the final brace.

This is a huge benefit to programmers: you can create an object and then know exactly where and when it will be destroyed and tidy itself up without the need for you to call delete. In standard C++ these objects are created in an area of memory called the stack, and so we say that these objects exhibit stack semantics.

Creating objects with stack semantics

In C++/CLI, you can create your objects in the same way, as you will see in the next exercise.

Note

In C++/CLI, these objects are not actually declared on the stack. This notation is a convenience that makes it possible for you to work with objects in the traditional C++ way, but under the hood, our objects are still created and managed by using handles.

  1. Continue using the project from the previous exercise.

  2. Edit the main function by adding code to create and use another object, placing it before the “end of program” WriteLine calls. Ensure that you create this object by using stack semantics.

    MyClass m2("m2");
    m2.DoSomething();
  3. Build and run the application.

    After the output for m1, you should see output similar to the following:

    Constructor called for m2
    DoSomething called for m2
    
    End of Program
    Destructor called for m2

You create and use the object, but do not manually delete it. The destructor is called automatically when execution passes the end curly bracket in the function.

Note

You can create most types of objects by using stack semantics, but you cannot do this for Strings or arrays. For those types you must use gcnew to get a handle, and you access the objects by using the -> operator.

Copy constructors

A copy constructor is a special kind of constructor function that takes an object of the same type as its argument. In other words, you can create an object as a copy of another one. In this section you’ll see how to write and use a copy constructor, but you’ll also learn about two other important concepts: dereferencing and tracking references.

Let’s start by analyzing what happens in the following piece of code:

ref class MyClass
{
   int value;
   String ^str;
public:
   MyClass(int v, String ^s) : value(v), str(s) {}
   int getValue() { return value; }
   String ^getString() { return str; }
};

int main(array<System::String ^> ^args)
{
    Console::WriteLine("Copy Construction");

   MyClass ^one = gcnew MyClass(3, "abc");
   MyClass ^two = one;

   Console::WriteLine("Value: {0}, str: {1}", two->getValue(), two->getString());

    return 0;
}

If you run this code, it prints out Value: 3, str: abc. The handle one points to a new MyClass object created through gcnew. The handle two is simply a copy of one; in other words, it points to the same object as one. Copying a handle doesn’t copy the object to which it points. And, if you modify the value member of two, the value for one will be changed, as well, because they are referring to the same object.

Suppose, though, that we did want to make two a copy of one. In that case, we would provide a copy constructor for the class, which would look like this:

MyClass(const MyClass %other)
{
    value = other.value;
    str = other.str;
}

The constructor takes another MyClass object and copies its members. The value is an int, so a copy of the value is made. The str member is a handle to a string, but because strings are immutable, it doesn’t matter that we’re pointing to the same one.

But, look more closely at the declaration of the argument: What is a const MyClass%? The percent (%) symbol introduces what is called a tracking reference. A handle lets you refer to an object indirectly, and you use the -> operator to access members. A tracking reference is really an alias, another name for a variable. Consider this code fragment:

int i = 5;
int %ri = i;   // ri is a tracking reference

Printing out ri prints “5”, because ri and i refer to the same variable. In many ways references are safer than handles because it is possible to have a handle that hasn’t been assigned, but it is difficult to create an uninitialized reference.

You can have references to built-in types, to managed objects, and to handles. When you have a tracking reference to a managed object, the runtime ensures that it always refers to the right location in memory, even if the garbage collector moves things around.

Note

In the same way that a handle is the C++/CLI version of a standard C++ pointer, a tracking reference is the C++/CLI version of a standard C++ reference. It differs from a standard reference because the garbage collector can relocate the object being referred to during memory compaction.

So, we now know that the copy constructor takes a tracking reference to an object rather than a handle. The reference is marked as const because it lets us make copies of constant MyClass objects, which the compiler otherwise would not allow.

The other construct that we need to cover is dereferencing. Here’s another code fragment:

MyClass ^m = gcnew MyClass();
MyClass %rm = *m;

The first line creates a MyClass object by using gcnew and returns a handle to it. The second line returns a reference to m by using the dereference operator, “*” (the asterisk character). You can read *m as “what m points to.”

However, this still hasn’t created a copy: m and rm are still referring to the same object in memory. But, what about this code?

MyClass mm = *m;

Here, mm is a MyClass with stack semantics, and the code is saying “create me a new object, mm, as a copy of the one to which m is pointing.” It is at this point that the copy constructor is invoked.

This exercise shows you how to implement a copy constructor for a class.

  1. Create a new CLR Console Application named CopyCon.

  2. Add the following class definition before the main function:

    ref class MyClass
    {
        int value;
        String ^str;
    public:
        MyClass(int v, String ^s) : value(v), str(s) {}
    
        MyClass(const MyClass %other)
        {
            Console::WriteLine("copy con called");
            value = other.value;
            str = other.str;
        }
    
        int getValue() { return value; }
        void setValue(int v) { value = v; }
        String ^getString() { return str; }
    };

    MyClass has two data members: an int and a String handle. The normal constructor initializes these two from the values passed in, and you can use the simple getter functions to retrieve the values later on.

  3. Implement the main function to create and use MyClass objects:

    int main(array<System::String ^> ^args)
    {
        Console::WriteLine("Copy Construction");
    
        MyClass ^one = gcnew MyClass(3, "abc");
        MyClass ^two = one;
    
        Console::WriteLine("Value: {0}, str: {1}", two->getValue(), two->getString());
    
        MyClass three = *one;
        three.setValue(4);
        Console::WriteLine("Value of one: {0}", one->getValue());
        Console::WriteLine("Value of three: {0}", three.getValue());
    
        return 0;
    }

    The handle one is created to point to a MyClass object, and the handle two is a copy of one. You can verify this by printing out the data by using the two handle. The object three is created by dereferencing one, which creates a copy. You can verify that this is the case by changing the data in three and showing that it hasn’t changed the data in one.

  4. Build and run the application. Check that you understand the output.

Relating objects with stack semantics

It is common for objects to be composed of other objects. For example, a Person might have an Address, or a Rectangle might be composed of two Points. Consider the Rectangle as an example. Because the Points are part of the Rectangle, it is reasonable to expect that when a Rectangle object is destroyed, its Points are destroyed, as well. If you declare the objects by using stack semantics, you can easily ensure that this happens.

In this exercise, you will see how to compose objects so that they are destroyed correctly.

  1. Create a new CLR Console Application project with a suitable name.

  2. Add a header file called Geometry.h to the project.

  3. Edit the header file to define two classes: Rectangle and Point. Note that a Rectangle is composed of two Points.

    using namespace System;
    
    ref class Point
    {
    public:
        Point();
        ~Point();
    };
    
    ref class Rectangle
    {
        Point p1, p2;
    public:
        Rectangle();
        ~Rectangle();
    };
  4. Add a source file called Geometry.cpp to the project and implement the Point and Rectangle class members.

    #include "stdafx.h"
    using namespace System;
    
    #include "Geometry.h"
    
    Point::Point()
    {
        Console::WriteLine("Point constructor called");
    }
    
    Point::~Point()
    {
        Console::WriteLine("Point destructor called");
    }
    
    Rectangle::Rectangle()
    {
        Console::WriteLine("Rectangle constructor called");
    }
    
    Rectangle::~Rectangle()
    {
        Console::WriteLine("Rectangle destructor called");
    }
  5. Edit main to create a Rectangle object by using stack semantics. Remember to add a #include for Geometry.h, as shown in the following:

    #include "Geometry.h"
    
    int main(array<System::String^>^ args)
    {
        Rectangle r;
    
        Console::WriteLine();
        Console::WriteLine("End of program");
        Console::WriteLine();
        return 0;
    }
  6. Build and run the application.

    You should see output similar to the following:

    Point constructor called
    Point constructor called
    Rectangle constructor called
    
    End of program
    
    Rectangle destructor called
    Point destructor called
    Point destructor called

You can see from this output that the Point members of the Rectangle are constructed before the Rectangle’s constructor is called. If you think about it, this is quite logical: when initializing itself, the Rectangle might want to use the Points to set some other properties, such as its area or diagonal length. So, it makes sense for the composed objects to be constructed before the constructor for the outer object is executed.

The destructors are called in reverse order, with the Rectangle destructor being called before the destructors for the Points. The Point objects are not destroyed until you can be sure that the Rectangle no longer needs them.

Note

If you want to create an object that takes no arguments in the constructor, do not put empty parentheses after the variable name.

Rectangle r();  // This won't work

If you do this, you will get a warning (C4930) and the application will not give the correct output when you run it. The reason is that the compiler takes this as a function prototype declaration rather than a variable declaration. It is not helpful behavior, but has been a part of traditional C++ since the earliest implementations.

When to use handles?

If you want a class to contain another object—as in the preceding Rectangle/Point exercise—you have a choice of how to represent the composed object. You could use an object, as you did in the exercise, or you could use a handle to an object, as in the following code.

ref class Rectangle
{
    Point ^p1;
    Point ^p2;
    ...
};

What is the difference between these two, and why might you choose one over the other?

The one you choose depends on the nature of the relationship between the two objects. It is beyond the scope of this book to give a full explanation of object-oriented design, but here are a couple of examples to introduce you to the ideas.

The questions you need to ask are the following:

  • Is the contained object a part of its container, such that it has no independent existence?

  • Is the contained object shared with anyone else?

  • Could you swap the contained object for another one?

  • Can the contained object live on after its container?

Consider the case of an object that represents a business meeting. This has properties such as description, date and time, but it also has a location, which is represented by a Location object. The Location object holds all the details about a meeting room: where it is, the phone number, how many people it can hold, whether it has conference facilities, and so on.

Obviously many meetings can use the same Location at different times, so they will have a reference to the same Location object. It is also possible that the meeting can be moved, so you need to be able to change the Location. And obviously, the Location doesn’t cease to exist when a meeting is over. This makes it a sensible idea to use a handle to a Location object in the Meeting class.

As a second example, consider the Rectangle/Point exercise again. The Points are parts of the Rectangle; they will disappear when the Rectangle object reaches the end of its life. There is no way that we are going to share a Point with anyone else, and so it makes sense that Points are contained within the Rectangle.

Quick reference

To

Do this

Define a destructor for a class.

Add a member function that has the same name as the class but prefixed with a tilde (~). For example:

MyClass::~MyClass()
{
    ...
}

Define a finalizer for a class.

Add a member function that has the same name as the class but prefixed with an exclamation mark (!). For example:

MyClass::!MyClass()
{
    ...
}

Destroy a dynamically created object.

Call delete on the handle to the object. For example:

MyClass ^m = gcnew MyClass();
...
delete m;

Create an object with stack semantics.

Declare it as you would a built-in type, passing any constructor arguments in parentheses. For example:

MyClass m1("argument1");

Create an object with stack semantics that has no arguments.

Declare it as you would a built-in type, but do not use empty parentheses. For example:

MyClass m3;    // correct
MyClass m4();   // wrong

Call methods on objects with stack semantics.

Use the dot operator. For example:

MyClass m5;
m5.DoSomething();

Compose an object that might be shared or changed.

Include them by using handles. For example:

ref class Meeting
{
    Location ^location;
    ...
};

Compose an object whose lifetime is bound to its container.

Include them by using stack semantics. For example:

ref class Rectangle
{
    Point p1;
    Point p2;
    ...
};
..................Content has been hidden....................

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