Chapter 8. More on Classes

WHAT YOU WILL LEARN IN THIS CHAPTER:

  • What a class destructor is and when and why it is necessary

  • How to implement a class destructor

  • How to allocate data members of a native C++ class in the free store and how to delete them when they are no longer required

  • When you must write a copy constructor for a class

  • What a union is and how it can be used

  • How to make objects of your class work with C++ operators such as + or *

  • How to use rvalue reference parameters to avoid unnecessary copying of class objects

  • What class templates are and how to define and use them

  • How to use the standard string class for string operations in native C++ programs

  • How to overload operators in C++/CLI classes

In this chapter, you will extend your knowledge of classes by understanding how you can make your class objects work more like the basic types in C++.

CLASS DESTRUCTORS

Although this section heading refers to destructors, it's also about dynamic memory allocation. When you allocate memory in the free store for class members, you are invariably obliged to make use of a destructor, in addition to a constructor, of course, and, as you'll see later in this chapter, using dynamically allocated class members will also require you to write your own copy constructor.

What Is a Destructor?

A destructor is a function that destroys an object when it is no longer required or when it goes out of scope. The class destructor is called automatically when an object goes out of scope. Destroying an object involves freeing the memory occupied by the data members of the object (except for static members, which continue to exist even when there are no class objects in existence). The destructor for a class is a member function with the same name as the class, preceded by a tilde (). The class destructor doesn't return a value and doesn't have parameters defined. For the CBox class, the prototype of the class destructor is:

~CBox();                 // Class destructor prototype

Because a destructor has no parameters, there can only ever be one destructor in a class.

Note

It's an error to specify a return value or parameters for a destructor.

The Default Destructor

All the objects that you have been using up to now have been destroyed automatically by the default destructor for the class. The default destructor is always generated automatically by the compiler if you do not define your own class destructor. The default destructor doesn't delete objects or object members that have been allocated in the free store by the operator new. If space for class members has been allocated dynamically in a contructor, then you must define your own destructor that will explicitly use the delete operator to release the memory that has been allocated by the constructor using the operator new, just as you would with ordinary variables. You need some practice in writing destructors, so let's try it out.

Destructors and Dynamic Memory Allocation

You will find that you often want to allocate memory for class data members dynamically. You can use the operator new in a constructor to allocate memory for an object member. In such a case, you must assume responsibility for releasing the memory when the object is no longer required by providing a suitable destructor. Let's first define a simple class where we can do this.

Suppose you want to define a class where each object is a message of some description — for example, a text string. The class should be as memory-efficient as possible, so, rather than defining a data member as a char array big enough to hold the maximum length string that you might require, you'll allocate memory in the free store for the message when an object is created. Here's the class definition:

//Listing 08_01
class CMessage
{
  private:
    char* pmessage;                   // Pointer to object text string

  public:

    // Function to display a message
void ShowIt() const
    {
      cout << endl << pmessage;
    }

    // Constructor definition
    CMessage(const char* text = "Default message")
    {
      pmessage = new char[strlen(text) + 1];        // Allocate space for text
      strcpy_s(pmessage, strlen(text) + 1, text);   // Copy text to new memory
    }

    ~CMessage();                               // Destructor prototype
};
                                                                     
Destructors and Dynamic Memory Allocation

This class has only one data member defined, pmessage, which is a pointer to a text string. This is defined in the private section of the class, so that it can't be accessed from outside the class.

In the public section, you have the ShowIt() function that will output a CMessage object to the screen. You also have the definition of a constructor and you have the prototype for the class destructor, ~CMessage(), which I'll come to in a moment.

The constructor for the class requires a string as an argument, but if none is passed, it uses the default string that is specified for the parameter. The constructor obtains the length of the string supplied as the argument, excluding the terminating NULL, by using the library function strlen(). For the constructor to use this library function, there must be a #include statement for the cstring header file. The constructor determines the number of bytes of memory necessary to store the string in the free store by adding 1 to the value that the function strlen() returns.

Note

Of course, if the memory allocation fails, an exception will be thrown that will terminate the program. If you wanted to manage such a failure to provide a more graceful end to the program, you would catch the exception within the constructor code. (See Chapter 6 for information on handling out-of-memory conditions.)

Having obtained the memory for the string using the operator new, you use the strcpy_s() library function that is also declared in the cstring header file to copy the string supplied as the argument to the constructor into the memory allocated for it. The strcpy_s() function copies the string specified by the third argument to the address contained in the first argument. The second argument specifies the length of the destination location.

You now need to write a class destructor that will free up the memory allocated for a message. If you don't provide a destructor for the class, there's no way to delete the memory allocated for an object. If you use this class as it stands in a program where a large number of CMessage objects are created, the free store will be gradually eaten away until the program fails. It's easy for this to occur in circumstances where it may not be obvious that it is happening. For example, if you create a temporary CMessage object in a function that is called many times in a program, you might assume that the objects are being destroyed at the return from the function. You'd be right about that, of course, but the free store memory will not be released. Thus, for each call of the function, more of the free store will be occupied by memory for discarded CMessage objects.

The code for the CMessage class destructor is as follows:

// Listing 08_02
// Destructor to free memory allocated by new
CMessage::~CMessage()
{
  cout << "Destructor called."    // Just to track what happens
       << endl;
  delete[] pmessage;              // Free memory assigned to pointer
}
                                                                  
Destructors and Dynamic Memory Allocation

Because you're defining the destructor outside of the class definition, you must qualify the name of the destructor with the class name, CMessage. All the destructor does is to first display a message so that you can see what's going on, and then use the delete operator to free the memory pointed to by the member pmessage. Note that you have to include the square brackets with delete because you're deleting an array (of type char).

IMPLEMENTING A COPY CONSTRUCTOR

When you allocate space for class members dynamically, there are demons lurking in the free store. For the CMessage class, the default copy constructor is woefully inadequate. Suppose you write these statements:

CMessage motto1("Radiation fades your genes.");
CMessage motto2(motto1);     // Calls the default copy constructor

The effect of the default copy constructor will be to copy the address that is stored in the pointer member of the class from motto1 to motto2 because the copying process implemented by the default copy constructor involves simply copying the values stored in the data members of the original object to the new object. Consequently, there will be only one text string shared between the two objects, as Figure 8-1 illustrates.

FIGURE 8-1

Figure 8.1. FIGURE 8-1

If the string is changed from either of the objects, it will be changed for the other object as well, because both objects share the same string. If motto1 is destroyed, the pointer in motto2 will be pointing at a memory area that has been released, and may now be used for something else, so chaos will surely ensue. Of course, the same problem arises if motto2 is deleted; motto1 would then contain a member pointing to a nonexistent string.

The solution is to supply a class copy constructor to replace the default version. You could implement this in the public section of the class as follows:

CMessage(const CMessage& aMess)
{
  size_t len = strlen(aMess.pmessage)+1;
  pmessage = new char[len];
  strcpy_s(pmessage, len, aMess.pmessage);
}

Remember from the previous chapter that, to avoid an infinite spiral of calls to the copy constructor, the parameter must be specified as a const reference. This copy constructor first allocates enough memory to hold the string in the object aMess, storing the address in the data member of the new object, and then copies the text string from the initializing object. Now, the new object will be identical to, but quite independent of, the old one.

Just because you don't initialize one CMessage class object with another, don't think that you're safe and need not bother with the copy constructor. Another monster lurks in the free store that can emerge to bite you when you least expect it. Consider the following statements:

CMessage thought("Eye awl weighs yews my spell checker.");
DisplayMessage(thought);     // Call a function to output a message

where the function DisplayMessage() is defined as:

void DisplayMessage(CMessage localMsg)
{
  cout << endl << "The message is: " << localMsg.ShowIt();
  return;
}

Looks simple enough, doesn't it? What could be wrong with that? A catastrophic error, that's what! What the function DisplayMessage() does is actually irrelevant. The problem lies with the parameter. The parameter is a CMessage object, so the argument in a call is passed by value. With the default copy constructor, the sequence of events is as follows:

  1. The object thought is created with the space for the message "Eye awl weighs yews my spell checker" allocated in the free store.

  2. The function DisplayMessage() is called and, because the argument is passed by value, a copy, localMsg, is made using the default copy constructor. Now, the pointer in the copy points to the same string in the free store as the original object.

  3. At the end of the function, the local object goes out of scope, so the destructor for the CMessage class is called. This deletes the local object (the copy) by deleting the memory pointed to by the pointer pmessage.

  4. On return from the function DisplayMessage(), the pointer in the original object, thought, still points to the memory area that has just been deleted. Next time you try to use the original object (or even if you don't, since it will need to be deleted sooner or later), your program will behave in weird and mysterious ways.

Any call to a function that passes by value an object of a class that has a member defined dynamically will cause problems. So, out of this, you have an absolutely 100-percent, 24-carat golden rule:

Note

If you allocate space for a member of a native C++ class dynamically, always implement a copy constructor.

SHARING MEMORY BETWEEN VARIABLES

As a relic of the days when 64KB was quite a lot of memory, you have a facility in C++ that allows more than one variable to share the same memory (but, obviously, not at the same time). This is called a union, and there are four basic ways in which you can use one:

  • You can use it so that a variable A occupies a block of memory at one point in a program, which is later occupied by another variable B of a different type, because A is no longer required. I recommend that you don't do this. It's not worth the risk of error that is implicit in such an arrangement. You can achieve the same effect by allocating memory dynamically.

  • Alternatively, you could have a situation in a program where a large array of data is required, but you don't know in advance of execution what the data type will be — it will be determined by the input data. I also recommend that you don't use unions in this case, since you can achieve the same result using a couple of pointers of different types and, again, allocating the memory dynamically.

  • A third possible use for a union is one that you may need now and again — when you want to interpret the same data in two or more different ways. This could happen when you have a variable that is of type long, and you want to treat it as two values of type short. Windows will sometimes package two short values in a single parameter of type long passed to a function. Another instance arises when you want to treat a block of memory containing numeric data as a string of bytes, just to move it around.

  • You can use a union as a means of passing an object or a data value around where you don't know in advance what its type is going to be. The union can provide for storing any one of the possible range of types that you might have.

Defining Unions

You define a union using the keyword union. It is best understood by taking an example of a definition:

union shareLD                // Sharing memory between long and double
{
  double dval;
  long lval;
};

This defines a union type shareLD that provides for the variables of type long and double to occupy the same memory. The union type name is usually referred to as a tag name. This statement is rather like a class definition, in that you haven't actually defined a union instance yet, so you don't have any variables at this point. Once the union type has been defined, you can define instances of a union in a declaration. For example:

shareLD myUnion;

This defines an instance of the union type, shareLD, that you defined previously. You could also have defined myUnion by including it in the union definition statement:

union shareLD                  // Sharing memory between long and double
{
  double dval;
  long lval;
} myUnion;

To refer to a member of the union, you use the direct member selection operator (the period) with the union instance name, just as you have done when accessing members of a class. So, you could set the long variable lval to 100 in the union instance MyUnion with this statement:

myUnion.lval = 100;            // Using a member of a union

Using a similar statement later in a program to initialize the double variable dval will overwrite lval. The basic problem with using a union to store different types of values in the same memory is that, because of the way a union works, you also need some means of determining which of the member values is current. This is usually achieved by maintaining another variable that acts as an indicator of the type of value stored.

A union is not limited to sharing between two variables. If you wish, you can share the same memory between several variables. The memory occupied by the union will be that which is required by its largest member. For example, suppose you define this union:

union shareDLF
{
  double dval;
  long lval;
  float fval;
} uinst = {1.5};
FIGURE 8-2

Figure 8.2. FIGURE 8-2

An instance of shareDLF will occupy 8 bytes, as illustrated in Figure 8-2.

In the example, you defined an instance of the union, uinst, as well as the tag name for the union. You also initialized the instance with the value 1.5.

Note

You can only initialize the first member of the union when you declare an instance.

Anonymous Unions

You can define a union without a union type name, in which case, an instance of the union is automatically declared. For example, suppose you define a union like this:

union
{
  char* pval;
  double dval;
  long lval;
};

This statement defines both a union with no name and an instance of the union with no name. Consequently, you can refer to the variables that it contains just by their names, as they appear in the union definition, pval, dval, and lval. This can be more convenient than a normal union with a type name, but you need to be careful that you don't confuse the union members with ordinary variables. The members of the union will still share the same memory. As an illustration of how the anonymous union above works, to use the double member, you could write this statement:

dval = 99.5;                   // Using a member of an anonymous union

As you can see, there's nothing to distinguish the variable dval as a union member. If you need to use anonymous unions, you could use a naming convention to make the members more obvious and thus make your code a little less obscure.

Unions in Classes and Structures

You can include an instance of a union in a class or in a structure. If you intend to store different types of value at different times, this usually necessitates maintaining a class data member to indicate what kind of value is stored in the union. There isn't usually a great deal to be gained by using unions as class or struct members.

OPERATOR OVERLOADING

Operator overloading is a very important capability because it enables you to make standard C++ operators, such as +, , *, and so on, work with objects of your own data types. It allows you to write a function that redefines a particular operator so that it performs a particular action when it's used with objects of a class. For example, you could redefine the operator > so that, when it was used with objects of the class CBox that you saw earlier, it would return true if the first CBox argument had a greater volume than the second.

Operator overloading doesn't allow you to invent new operators, nor can you change the precedence of an operator, so your overloaded version of an operator will have the same priority in the sequence of evaluating an expression as the original base operator. The operator precedence table can be found in Chapter 2 of this book and in the MSDN Library.

Although you can't overload all the operators, the restrictions aren't particularly oppressive. These are the operators that you can't overload:

::

The scope resolution operator

?:

The conditional operator

.

The direct member selection operator

sizeof

The size-of operator

.*

The de-reference pointer to class member operator

Anything else is fair game, which gives you quite a bit of scope. Obviously, it's a good idea to ensure that your versions of the standard operators are reasonably consistent with their normal usage, or at least reasonably intuitive in their operation. It wouldn't be a very sensible approach to produce an overloaded + operator for a class that performed the equivalent of a multiply on class objects. The best way to understand how operator overloading works is to work through an example, so let's implement what I just referred to, the greater-than operator, >, for the CBox class.

Implementing an Overloaded Operator

To implement an overloaded operator for a class, you have to write a special function. Assuming that it is a member of the class CBox, the declaration for the function to overload the > operator within the class definition will be as follows:

class CBox
{
  public:
    bool operator>(CBox& aBox) const;  // Overloaded 'greater than'

  // Rest of the class definition...
};

The word operator here is a keyword. Combined with an operator symbol or name, in this case, >, it defines an operator function. The function name in this case is operator>. You can write an operator function with or without a space between the keyword operator and the operator itself, as long as there's no ambiguity. The ambiguity arises with operators with names rather than symbols such as new or delete. If you were to write operatornew and operatordelete without a space, they are legal names for ordinary functions, so for operator functions with these operators, you must leave a space between the keyword operator and the operator name itself. The strangest-looking function name for an overloaded operator function is operator()(). This looks like a typing error, but it is, in fact, a function that overloads the function call operator, (). Note that you declare the operator>() function as const because it doesn't modify the data members of the class.

With the operator>() operator function, the right operand of the operator will be defined by the function parameter. The left operand will be defined implicitly by the pointer this. So, if you have the following if statement:

if(box1 > box2)
   cout << endl << "box1 is greater than box2";

then the expression between the parentheses in the if will call our operator function, and is equivalent to this function call:

box1.operator>(box2);

The correspondence between the CBox objects in the expression and the operator function parameters is illustrated in Figure 8-3.

FIGURE 8-3

Figure 8.3. FIGURE 8-3

Let's look at how the code for the operator>() function works:

// Operator function for 'greater than' which
// compares volumes of CBox objects.
bool CBox::operator>(const CBox& aBox) const
{
  return this->Volume() > aBox.Volume();
}

You use a reference parameter to the function to avoid unnecessary copying when the function is called. Because the function does not alter the object for which it is called, you can declare it as const. If you don't do this, you cannot use the operator to compare const objects of type CBox at all.

The return expression uses the member function Volume() to calculate the volume of the CBox object pointed to by this, and compares the result with the volume of the object aBox using the basic operator >. The basic > operator returns a value of type int (not a type bool), and thus, 1 is returned if the CBox object pointed to by the pointer this has a larger volume than the object aBox passed as a reference argument, and 0 otherwise. The value that results from the comparison will be automatically converted to the return type of the operator function, type bool.

Implementing Full Support for a Comparison Operator

With the current version of the operator function operator>(), there are still a lot of things that you can't do. Specifying a problem solution in terms of CBox objects might well involve statements such as the following:

if(aBox > 20.0)
  // Do something...

Our function won't deal with that. If you try to use an expression comparing a CBox object with a numerical value, you'll get an error message. To support this capability, you would need to write another version of the operator>() function as an overloaded function.

You can quite easily support the type of expression that you've just seen. The declaration of the member function within the class would be:

// Compare a CBox object with a constant
bool operator>(const double& value) const;

This would appear in the definition of the class, and the right operand for the > operator corresponds to the function parameter here. The CBox object that is the left operand will be passed as the implicit pointer this.

The implementation of this overloaded operator is also easy. It's just one statement in the body of the function:

// Function to compare a CBox object with a constant
bool CBox::operator>(const double& value) const
{
  return this->Volume() > value;
}

This couldn't be much simpler, could it? But you still have a problem using the > operator with CBox objects. You may well want to write statements such as this:

if(20.0 > aBox)
   // do something...

You might argue that this could be done by implementing the operator<() operator function that accepted a right argument of type double, and rewriting the statement above to use it, which is quite true. Indeed, implementing the < operator is likely to be a requirement for comparing CBox objects, anyway, but an implementation of support for an object type shouldn't artificially restrict the ways in which you can use the objects in an expression. The use of the objects should be as natural as possible. The problem is how to do it.

A member operator function always provides the left argument as the pointer this. Because the left argument, in this case, is of type double, you can't implement it as a member function. That leaves you with two choices: an ordinary function or a friend function. Because you don't need to access the private members of the class, it doesn't need to be a friend function, so you can implement the overloaded > operator with a left operand of type double as an ordinary function. The prototype would need to be:

bool operator>(const double& value, const CBox& aBox);

This is placed outside the class definition, of course, because the function isn't a member of the CBox class. The implementation would be this:

// Function comparing a constant with a CBox object
bool operator>(const double& value, const CBox& aBox)
{
   return value > aBox.Volume();
}

As you have seen already, an ordinary function (and a friend function, too, for that matter) accesses the members of an object by using the direct member selection operator and the object name. Of course, an ordinary function only has access to the public members. The member function Volume() is public, so there's no problem using it here.

If the class didn't have the public function Volume(), you could either declare the operator function a friend function that could access the private data members directly, or you could provide a set of member functions to return the values of the private data members and use those in an ordinary function to implement the comparison.

Note

Any comparison operator can be implemented in much the same way as you have implemented these. They would only differ in the minor details, and the general approach to implementing them would be exactly the same.

Overloading the Assignment Operator

If you don't provide an overloaded assignment operator function for your class, the compiler will provide a default. The default version will simply provide a member-by-member copying process, similar to that of the default copy constructor. However, don't confuse the default copy constructor with the default assignment operator. The default copy constructor is called by a declaration of a class object that's initialized with an existing object of the same class, or by passing an object to a function by value. The default assignment operator, on the other hand, is called when the left side and the right side of an assignment statement are objects of the same class type.

For the CBox class, the default assignment operator works with no problem, but for any class which has space for members allocated dynamically, you need to look carefully at the requirements of the class in question. There may be considerable potential for chaos in your program if you leave the assignment operator out under these circumstances.

For a moment, let's return to the CMessage class that you used when I was talking about copy constructors. You'll remember it had a member, pmessage, that was a pointer to a string. Now, consider the effect that the default assignment operator could have. Suppose you had two instances of the class, motto1 and motto2. You could try setting the members of motto2 equal to the members of motto1 using the default assignment operator, as follows:

motto2 = motto1;                 // Use default assignment operator

The effect of using the default assignment operator for this class is essentially the same as using the default copy constructor: disaster will result! Since each object will have a pointer to the same string, if the string is changed for one object, it's changed for both. There is also the problem that when one of the instances of the class is destroyed, its destructor will free the memory used for the string, and the other object will be left with a pointer to memory that may now be used for something else.

What you need the assignment operator to do is to copy the text to a memory area owned by the destination object.

Fixing the Problem

You can fix this with your own assignment operator function, which we will assume is defined within the class definition:

// Overloaded assignment operator for CMessage objects
CMessage& operator=(const CMessage& aMess)
{
  // Release memory for 1st operand
  delete[] pmessage;
  pmessage = new char[strlen(aMess.pmessage) + 1];

  // Copy 2nd operand string to 1st
  strcpy_s(this->pmessage, strlen(aMess.pmessage) + 1, aMess.pmessage);

  // Return a reference to 1st operand
  return *this;
}

An assignment might seem very simple, but there's a couple of subtleties that need further investigation. First of all, note that you return a reference from the assignment operator function. It may not be immediately apparent why this is so — after all, the function does complete the assignment operation entirely, and the object on the right of the assignment will be copied to that on the left. Superficially, this would suggest that you don't need to return anything, but you need to consider in a little more depth how the operator might be used.

There's a possibility that you might need to use the result of an assignment operation on the right-hand side of an expression. Consider a statement such as this:

motto1 = motto2 = motto3;

Because the assignment operator is right-associative, the assignment of motto3 to motto2 will be carried out first, so this will translate into the following statement:

motto1 = (motto2.operator=(motto3));

The result of the operator function call here is on the right of the equals sign, so the statement will finally become this:

motto1.operator=(motto2.operator=(motto3));

If this is to work, you certainly have to return something. The call of the operator=() function between the parentheses must return an object that can be used as an argument to the other operator=() function call. In this case, a return type of either CMessage or CMessage& would do it, so a reference is not mandatory in this situation, but you must at least return a CMessage object.

However, consider the following example:

(motto1 = motto2) = motto3;

This is perfectly legitimate code — the parentheses serve to make sure the leftmost assignment is carried out first. This translates into the following statement:

(motto1.operator=(motto2)) = motto3;

When you express the remaining assignment operation as the explicit overloaded function call, this ultimately becomes:

(motto1.operator=(motto2)).operator=(motto3);

Now, you have a situation where the object returned from the operator=() function is used to call the operator=() function. If the return type is just CMessage, this will not be legal because a temporary copy of the original object is actually returned, and the compiler will not allow a member function call using a temporary object. In other words, the return value when the return type is CMessage is not an lvalue. The only way to ensure this sort of thing will compile and work correctly is to return a reference, which is an lvalue, so the only possible return type if you want to allow fully flexible use of the assignment operator with your class objects is CMessage&.

Note that the native C++ language does not enforce any restrictions on the accepted parameter or return types for the assignment operator, but it makes sense to declare the operator in the way I have just described if you want your assignment operator functions to support normal C++ usage of assignment.

The second subtlety you need to keep in mind is that each object already has memory for a string allocated, so the first thing that the operator function has to do is to delete the memory allocated to the first object and reallocate sufficient memory to accommodate the string belonging to the second object. Once this is done, the string from the second object can be copied to the new memory now owned by the first.

There's still a defect in this operator function. What if you were to write the following statement?

motto1 = motto1;

Obviously, you wouldn't do anything as stupid as this directly, but it could easily be hidden behind a pointer, for instance, as in the following statement,

Motto1 = *pMess;

If the pointer pMess points to motto1, you essentially have the preceding assignment statement. In this case, the operator function as it stands would delete the memory for motto1, allocate some more memory based on the length of the string that has already been deleted, and try to copy the old memory, which, by then, could well have been corrupted. You can fix this with a check for identical left and right operands at the beginning of the function, so now, the definition of the operator=() function would become this:

// Overloaded assignment operator for CMessage objects
CMessage& operator=(const CMessage& aMess)
{
  if(this == &aMess)                   // Check addresses, if equal
    return *this;                      // return the 1st operand

  // Release memory for 1st operand
  delete[] pmessage;
  pmessage = new char[strlen(aMess.pmessage) + 1];

  // Copy 2nd operand string to 1st
  strcpy_s(this->pmessage, strlen(aMess.pmessage)+1, aMess.pmessage);

  // Return a reference to 1st operand
  return *this;
}

This code assumes that the function definition appears within the class definition.

Overloading the Addition Operator

Let's look at overloading the addition operator for our CBox class. This is interesting because it involves creating and returning a new object. The new object will be the sum (whatever you define that to mean) of the two CBox objects that are its operands.

So what do we want the sum of two boxes to mean? Well, there are quite a few legitimate possibilities, but we'll keep it simple here. Let's define the sum of two CBox objects as a CBox object that is large enough to contain the other two boxes stacked on top of each other. You can do this by making the new object have an m_Length member that is the larger of the m_Length members of the objects being added, and an m_Width member derived in a similar way. The m_Height member will be the sum of the m_Height members of the two operand objects, so that the resultant CBox object can contain the other two CBox objects. This isn't necessarily an optimal solution, but it will be sufficient for our purposes. By altering the constructor, we'll also arrange that the m_Length member of a CBox object is always greater than or equal to the m_Width member.

Our version of the addition operation for boxes is easier to explain graphically, so it's illustrated in Figure 8-4.

FIGURE 8-4

Figure 8.4. FIGURE 8-4

Because you need to get at the members of a CBox object directly, you will make the operator+() a member function. The declaration of the function member within the class definition will be this:

CBox operator+(const CBox& aBox) const; // Function adding two CBox objects

You define the parameter as a reference to avoid unnecessary copying of the right argument when the function is called, and you make it a const reference because the function does not modify the argument. If you don't declare the parameter as a const reference, the compiler will not allow a const object to be passed to the function, so it would then not be possible for the right operand of + to be a const CBox object. You also declare the function as const as it doesn't change the object for which it is called. Without this, the left operand of + could not be a const CBox object.

The operator+() function definition would now be as follows:

// Function to add two CBox objects
CBox CBox::operator+(const CBox& aBox) const
{
  // New object has larger length and width, and sum of heights
  return CBox(m_Length > aBox.m_Length ? m_Length : aBox.m_Length,
              m_Width  > aBox.m_Width  ? m_Width  : aBox.m_Width,
              m_Height + aBox.m_Height);
}

You construct a local CBox object from the current object (*this) and the object that is passed as the argument, aBox. Remember that the return process will make a temporary copy of the local object and that is what is passed back to the calling function, not the local object, which is discarded on return from the function.

Overloading the Increment and Decrement Operators

I'll briefly introduce the mechanism for overloading the increment and decrement operators in a class because they have some special characteristics that make them different from other unary operators. You need a way to deal with the fact that the ++ and −− operators come in a prefix and postfix form, and the effect is different depending on whether the operator is applied in its prefix or postfix form. In native C++, the overloaded operator is different for the prefix and postfix forms of the increment and decrement operators. Here's how they would be defined in a class with the name Length, for example:

class Length
{
  private:
    double len;                        // Length value for the class

  public:
    Length& operator++();              // Prefix increment operator
    const Length operator++(int);      // Postfix increment operator

    Length& operator--();              // Prefix decrement operator
    const Length operator--(int);      // Postfix decrement operator

  // rest of the class...

}

This simple class assumes a length is stored just as a value of type double. You would probably, in reality, make a length class more sophisticated than this, but it will serve to illustrate how you overload the increment and decrement operators.

The primary way the prefix and postfix forms of the overloaded operators are differentiated is by the parameter list; for the prefix form, there are no parameters, and for the postfix form, there is a parameter of type int. The parameter in the postfix operator function is only to distinguish it from the prefix form and is otherwise unused in the function implementation.

The prefix increment and decrement operators increment or decrement the operand before its value is used in an expression, so you just return a reference to the current object after it has been incremented or decremented. Here's how an implementation of the prefix operator++() function would look for the Length class:

Length& Length::operator++()
{
  ++(this->len);
  return *this;
}

With the postfix forms, the operand is incremented after its current value is used in an expression. This is achieved by creating a new object that is a copy of the current object before incrementing the current object and returning the copy after the current object has been modified. Here's how you might implement the function to overload the postfix ++ operator for the Length class:

const Length Length::operator++(int)
{
  Length length = *this;               // Copy the current object
  ++*this;                             // Increment the current object
  return length;                       // Return the original copy
}

After copying the current object, you increment it using the prefix ++ operator for the class. You then return the original, unincremented copy of the current object; it is this value that will be used in the expression in which the operator appears. Specifying the return value as const prevents expressions such as data++++ from compiling.

Overloading the Function Call Operator

The function call operator is (), so the function overload for this is operator()(). An object of a class that overloads the function call operator is referred to as a function object or functor because you can use the object name as though it is the name of a function. Let's look at a simple example. Here's a class that overloads the function call operator:

class Area
{
  public:
  int operator()(int length, int width) { return length*width; }
};

The operator function in this class calculates an area as the product of its integer arguments. To use this operator function, you just need to create an object of type Area, for example:

Area area;                                        // Create function object
int pitchLength(100), pitchWidth(50);
int pitchArea = area(pitchLength, pitchWidth);    // Execute function call overload

The first statement creates the area object that is used in the third statement to call the function call operator for the object. This returns the area of a football pitch, in this case.

Of course, you can pass a function object to another function, just as you would any other object. Look at this function:

void printArea(int length, int width, Area& area)
{
  cout << "Area is " << area(length, width);
}

Here is a statement that uses this function:

printArea(20, 35, Area());

This statement calls the printArea() function with the first two arguments specifying the length and width of a rectangle. The third argument calls the default constructor to create an Area object that is used in the function to calculate an area. Thus, a function object provides you with a way of passing a function as an argument to another function that is simpler and easier to work with than using pointers to functions.

Classes that define function objects typically do not need data members and do not have a constructor defined, so there is minimal overhead in creating and using function objects. Function object classes are also usually defined as templates because this makes them very flexible, as you'll see later in this chapter.

THE OBJECT COPYING PROBLEM

Copying is implicit in passing arguments by value to a function. This is not a problem when the argument is of a fundamental type, but for arguments that are objects of a class type, it can be. The overhead arising from the copy operation for an object can be considerable, especially when the object owns memory that was allocated dynamically. Copying an object is achieved by calling the class copy constructor, so the efficiency of this class function is critical to execution performance. As you have seen with the CMessage class, the assignment operator also involves copying an object. However, there can be circumstances where such copy operations are not really necessary, and if you can find a way to avoid them in such situations, execution time may be substantially reduced. Rvalue reference parameters are the key to making this possible.

Avoiding Unnecessary Copy Operations

A modified version of the CMessage class from Ex8_05.cpp will provide a basis for seeing how this works. Here's a version of the class that implements the addition operator:

class CMessage
{
  private:
    char* pmessage;                    // Pointer to object text string

  public:
    // Function to display a message
    void ShowIt() const
    {
      cout << endl << pmessage;
    }

    // Overloaded addition operator
    CMessage operator+(const CMessage& aMess) const
    {
      cout << "Add operator function called." << endl;
      size_t len = strlen(pmessage) + strlen(aMess.pmessage) + 1;
      CMessage message;
      message.pmessage = new char[len];
      strcpy_s(message.pmessage, len, pmessage);
      strcat_s(message.pmessage, len, aMess.pmessage);
      return message;
    }

    // Overloaded assignment operator for CMessage objects
    CMessage& operator=(const CMessage& aMess)
{

      cout << "Assignment operator function called." << endl;
      if(this == &aMess)               // Check addresses, if equal
        return *this;                  // return the 1st operand

      // Release memory for 1st operand
      delete[] pmessage;
      pmessage = new char[strlen(aMess.pmessage) + 1];

      // Copy 2nd operand string to 1st
      strcpy_s(this->pmessage, strlen(aMess.pmessage)+1, aMess.pmessage);

      // Return a reference to 1st operand
      return *this;
    }

    // Constructor definition
    CMessage(const char* text = "Default message")
    {
      cout << "Constructor called." << endl;
      pmessage = new char[strlen(text) + 1];         // Allocate space for text
      strcpy_s(pmessage, strlen(text)+1, text);      // Copy text to new memory
    }

    // Copy constructor definition
    CMessage(const CMessage& aMess)
    {
      cout << "Copy constructor called." << endl;
      size_t len = strlen(aMess.pmessage)+1;
      pmessage = new char[len];
      strcpy_s(pmessage, len, aMess.pmessage);
    }

    // Destructor to free memory allocated by new
    ~CMessage()
    {
      cout << "Destructor called."     // Just to track what happens
           << endl;
      delete[] pmessage;               // Free memory assigned to pointer
    }
};
                                                                   
Avoiding Unnecessary Copy Operations

The changes from the version in Ex8_05.cpp are highlighted. There is now output from the constructor and the assignment operator function to trace when they are called. There is also a copy constructor and the addition operator function in the class. The operator+() function is for adding two CMessage objects. You could add versions for concatenating a CMessage object with a string literal, but it's not necessary for our purposes here.

Let's see what copying occurs with some simple operations on CMessage objects.

Applying Rvalue Reference Parameters

When the source object is a temporary object that is going to be destroyed immediately after the copy operation, the alternative to copying is to steal the heap memory belonging to the temporary object that is pointed to by its pmessage member and transfer it to the destination object. If you can do this, you avoid the need to allocate more heap memory for the destination object, and there will be no need to release the memory owned by the source object. The source object is going to be destroyed immediately after the operation, so there is no risk in doing this — just faster execution. The key to being able to perform this trick is to detect when the source object in a copy operation is an rvalue. This is exactly what an rvalue reference parameter enables you to do.

You can create an additional overload for the operator=() function like this:

CMessage& operator=(CMessage&& aMess)
{
  cout << "Move assignment operator function called." << endl;
  delete[] pmessage;            // Release memory for left operand
  pmessage = aMess.pmessage;    // Steal string from rhs object
  aMess.pmessage = nullptr;     // Null rhs pointer
  return *this;                 // Return a reference to 1st operand
}
                                                                   
Applying Rvalue Reference Parameters

This operator function will be called when the right operand is an rvalue — a temporary object. When the right operand is an lvalue, the original function with an lvalue reference parameter will be called. The rvalue reference version of the function deletes the string pointed to by the pmessage member of the destination object and copies the address stored in the pmessage member of the source object. The pmessage member of the source object is then set to nullptr. It is essential that you do this; otherwise, the message would be deleted by the destructor call for the source object. Note that you must not specify the parameter as const in this case, because you are modifying it.

You can apply exactly the same logic to copy constructor operations by adding an overloaded copy constructor with an rvalue reference parameter:

CMessage(CMessage&& aMess)
{
  cout << "Move copy constructor called." << endl;
  pmessage = aMess.pmessage;
  aMess.pmessage = nullptr;
}
                                                                   
Applying Rvalue Reference Parameters

Instead of copying the message belonging to the source object to the object being constructed, you simply transfer the address of the message string from the source object to the new object, so, in this case, the copy is just a move operation. As before, you set pmessage for the source object to nullptr to prevent the message string from being deleted by the destructor.

Note

If you define the operator=() function and copy constructor in a class with the parameters as non-const rvalue references, make sure you also define the standard versions with const lvalue reference parameters. If you don't, you will get the compiler-supplied default versions of these that perform member-by-member copying. This will certainly not be what you want.

Named Objects are Lvalues

When the assignment operator function with the rvalue reference parameter in the CMessage class is called, we know for certain that the argument — the right operand — is an rvalue and is, therefore, a temporary object from which we can steal memory. However, within the body of this operator function, the parameter, aMess, is an lvalue. This is because any expression that is a named variable is an lvalue. This can result in inefficiencies creeping back in, as I can demonstrate through an example that uses a modified version of the CMessage class:

class CMessage
{
  private:
    CText text;                    // Object text string

  public:
    // Function to display a message
    void ShowIt() const
    {
      text.ShowIt();
    }

    // Overloaded addition operator
    CMessage operator+(const CMessage& aMess) const
    {
      cout << "CMessage add operator function called." << endl;
      CMessage message;
      message.text = text + aMess.text;
      return message;
    }

    // Copy assignment operator for CMessage objects
    CMessage& operator=(const CMessage& aMess)
    {
      cout << "CMessage copy assignment operator function called." << endl;
      if(this == &aMess)               // Check addresses, if equal
        return *this;                  // return the 1st operand

      text = aMess.text;
      return *this;                    // Return a reference to 1st operand
    }

    // Move assignment operator for CMessage objects
    CMessage& operator=(CMessage&& aMess)
    {
      cout << "CMessage move assignment operator function called." << endl;
      text = aMess.text;
return *this;                    // Return a reference to 1st operand
    }

    // Constructor definition
    CMessage(const char* str = "Default message")
    {
      cout << "CMessage constructor called." << endl;
      text = CText(str);
    }

    // Copy constructor definition
    CMessage(const CMessage& aMess)
    {
      cout << "CMessage copy constructor called." << endl;
      text = aMess.text;
    }

    // Move constructor definition
    CMessage(CMessage&& aMess)
    {
      cout << "CMessage move constructor called." << endl;
      text = aMess.text;
    }
};
                                                                    
Named Objects are Lvalues

The text for the message is now stored as an object of type CText, and the member functions of the CMessage class have been changed accordingly. Note that the class is kitted out with rvalue reference versions of the copy constructor and the assignment operator, so it should move rather than create new objects when it is feasible to do so. Here's the definition of the CText class:

class CText
{
private:
  char* pText;

public:
    // Function to display text
    void ShowIt() const
    {
      cout << pText << endl;
    }

    // Constructor
    CText(const char* pStr="No text")
    {
      cout << "CText constructor called." << endl;
      size_t len(strlen(pStr)+1);
      pText = new char[len];                         // Allocate space for text
      strcpy_s(pText, len, pStr);                    // Copy text to new memory
    }

    // Copy constructor definition
    CText(const CText& txt)
{
      cout << "CText copy constructor called." << endl;
      size_t len(strlen(txt.pText)+1);
      pText = new char[len];
      strcpy_s(pText, len, txt.pText);
    }

    // Move constructor definition
    CText(CText&& txt)
    {
      cout << "CText move constructor called." << endl;
      pText = txt.pText;
      txt.pText = nullptr;
    }

    // Destructor to free memory allocated by new
    ~CText()
    {
      cout << "CText destructor called." << endl;    // Just to track what happens
      delete[] pText;                                // Free memory
    }

    // Assignment operator for CText objects
    CText& operator=(const CText& txt)
    {
      cout << "CText assignment operator function called." << endl;
      if(this == &txt)                               // Check addresses, if equal
        return *this;                                // return the 1st operand

      delete[] pText;                          // Release memory for 1st operand
      size_t len(strlen(txt.pText)+1);
      pText = new char[len];

      // Copy 2nd operand string to 1st
      strcpy_s(this->pText, len, txt.pText);
      return *this;                            // Return a reference to 1st operand
    }

    // Move assignment operator for CText objects
    CText& operator=(CText&& txt)
    {
      cout << "CText move assignment operator function called." << endl;
      delete[] pText;                          // Release memory for 1st operand
      pText = txt.pText;
      txt.pText = nullptr;
      return *this;                            // Return a reference to 1st operand
    }

    // Overloaded addition operator
    CText operator+(const CText& txt) const
    {
      cout << "CText add operator function called." << endl;
      size_t len(strlen(pText) + strlen(txt.pText) + 1);
      CText aText;
      aText.pText = new char[len];
      strcpy_s(aText.pText, len, pText);
strcat_s(aText.pText, len, txt.pText);
      return aText;
    }
};
                                                                  
Named Objects are Lvalues

It looks like a lot of code, but this is because the class has overloaded versions of the copy constructor and the assignment operator, and it has the operator+() function defined. The CMessage class makes use of these in the implementation of its member functions. There are also output statements to trace when each function is called. Let's exercise these classes with an example.

CLASS TEMPLATES

You saw back in Chapter 6 that you could define a function template that would automatically generate functions varying in the type of arguments accepted, or in the type of values returned. C++ has a similar mechanism for classes. A class template is not, in itself, a class, but a sort of "recipe" for a class that will be used by the compiler to generate the code for a class. As you can see from Figure 8-5, it's like the function template — you determine the class that you want generated by specifying your choice of type for the parameter (T, in this case) that appears between the angled brackets in the template. Doing this generates a particular class that is referred to as an instance of the class template. The process of creating a class from a template is described as instantiating the template.

FIGURE 8-5

Figure 8.5. FIGURE 8-5

An appropriate class definition is generated when you instantiate an object of a template class for a particular type, so you can generate any number of different classes from one class template. You'll get a good idea of how this works in practice by looking at an example.

Defining a Class Template

I'll choose a simple example to illustrate how you define and use a class template, and I won't complicate things by worrying too much about possible errors that can arise if it's misused. Suppose you want to define classes that can store a number of data samples of some kind, and each class is to provide a Max() function to determine the maximum sample value of those stored. This function will be similar to the one you saw in the function template discussion in Chapter 6. You can define a class template, which will generate a class CSamples to store samples of whatever type you want:

template <class T>
class CSamples
{
  public:
    // Constructor definition to accept an array of samples
    CSamples(const T values[], int count)
    {
      m_Free = count < 100 ? Count : 100; // Don't exceed the array
      for(int i = 0; i < m_Free; i++)
        m_Values[i] = values[i];          // Store count number of samples
    }

    // Constructor to accept a single sample
    CSamples(const T& value)
    {
      m_Values[0] = value;             // Store the sample
      m_Free = 1;                      // Next is free
    }

    // Default constructor
    CSamples(){ m_Free = 0 }           // Nothing stored, so first is free

    // Function to add a sample
    bool Add(const T& value)
    {
      bool OK = m_Free < 100;          // Indicates there is a free place
      if(OK)
        m_Values[m_Free++] = value;    // OK true, so store the value
      return OK;
    }

    // Function to obtain maximum sample
    T Max() const
    {
      // Set first sample or 0 as maximum
      T theMax = m_Free ? m_Values[0] : 0;

      for(int i = 1; i < m_Free; i++)  // Check all the samples
        if(m_Values[i] > theMax)
          theMax = m_Values[i];        // Store any larger sample
return theMax;
    }

private:
    T m_Values[100];              // Array to store samples
    int m_Free;                   // Index of free location in m_Values
};

To indicate that you are defining a template, rather than a straightforward class, you insert the template keyword and the type parameter, T, between angled brackets, just before the class keyword and the class name, CSamples. This is essentially the same syntax that you used to define a function template back in Chapter 6. The parameter T is the type variable that will be replaced by a specific type when you declare a class object. Wherever the parameter T appears in the class definition, it will be replaced by the type that you specify in your object declaration; this creates a class definition corresponding to this type. You can specify any type (a basic data type or a class type), but it has to make sense in the context of the class template, of course. Any class type that you use to instantiate a class from a template must have all the operators defined that the member functions of the template will use with such objects. If your class hasn't implemented operator>(), for example, it will not work with the CSamples class template above. In general, you can specify multiple parameters in a class template if you need them. I'll come back to this possibility a little later in the chapter.

Getting back to the example, the type of the array in which the samples will be stored is specified as T. The array will, therefore, be an array of whatever type you specify for T when you declare a CSamples object. As you can see, you also use the type T in two of the constructors for the class, as well as in the Add() and Max() functions. Each of these occurrences will also be replaced when you instantiate a class object using the template.

The constructors support the creation of an empty object, an object with a single sample, and an object initialized with an array of samples. The Add() function allows samples to be added to an object one at a time. You could also overload this function to add an array of samples. The class template includes some elementary provision to prevent the capacity of the m_Values array being exceeded in the Add() function, and in the constructor that accepts an array of samples.

As I said earlier, in theory, you can create objects of CSamples classes that will handle any data type: type int, type double, or any class type that you've defined. In practice, this doesn't mean it will necessarily compile and work as you expect. It all depends on what the template definition does, and usually, a template will only work for a particular range of types. For example, the Max() function implicitly assumes that the > operator is available for whatever type is being processed. If it isn't, your program will not compile. Clearly, you'll usually be in the position of defining a template that works for some types but not others, but there's no way you can restrict what type is applied to a template.

Template Member Functions

You may want to place the definition of a class template member function outside of the template definition. The syntax for this isn't particularly obvious, so let's look at how you do it. You put the function declaration in the class template definition in the normal way. For instance:

template <class T>
class CSamples
{
// Rest of the template definition...
  T Max() const;             // Function to obtain maximum sample
  // Rest of the template definition...
}

This declares the Max() function as a member of the class template, but doesn't define it. You now need to create a separate function template for the definition of the member function. You must use the template class name plus the parameters in angled brackets to identify the class template to which the function template belongs:

template<class T>
T CSamples<T>::Max() const
{
  // Set first sample or 0 as maximum
  T theMax = m_Free ? m_Values[0] : 0;

  for(int i = 1; i < m_Free; i++)         // Check all the samples
    if(m_Values[i] > theMax)
      theMax = m_Values[i];               // Store any larger sample
  return theMax;
}

You saw the syntax for a function template back in Chapter 6. Since this function template is for a member of the class template with the parameter T, the function template definition here should have the same parameters as the class template definition. There's just one in this case — T — but in general, there can be several. If the class template had two or more parameters, then so would each template defining a member function.

Note how you only put the parameter name, T, along with the class name before the scope resolution operator. This is necessary — the parameters are fundamental to the identification of the class to which a function, produced from the template, belongs. The type will be CSamples<T> with whatever type you assign to T when you create an instance of the class template. Your type is plugged into the class template to generate the class definition, and into the function template to generate the definition for the Max() function for the class. Each class that's produced from the class template needs to have its own definition for the function Max().

Defining a constructor or a destructor outside of the class template definition is very similar. You could write the definition of the constructor that accepts an array of samples as:

template<class T>
CSamples<T>::CSamples(const T values[], int count)
{
  m_Free = count < 100 ? count : 100;   // Don't exceed the array

  for(int i = 0; i < m_Free; i++)
    m_Values[i] = values[i];            // Store count number of samples
}

The class to which the constructor belongs is specified in the template in the same way as for an ordinary member function. Note that the constructor name doesn't require the parameter specification — it is just CSamples, but it needs to be qualified by the class template type CSamples<T>. You only use the parameter with the class template name preceding the scope resolution operator.

Creating Objects from a Class Template

When you use a function defined by a function template, the compiler is able to generate the function from the types of the arguments used. The type parameter for the function template is implicitly defined by the specific use of a particular function. Class templates are a little different. To create an object based on a class template, you must always specify the type parameter following the class name in the declaration.

For example, to declare a CSamples<> object to handle samples of type double, you could write the declaration as:

CSamples<double> myData(10.0);

This defines an object of type CSamples<double> that can store samples of type double, and the object is created with one sample stored with the value 10.0.

Note

You might be surprised at what happens if you add some output statements to the class constructors. The constructor for the CBox is being called 103 times! Look at what is happening in the main() function. First, you create an array of three CBox objects, so that's three calls. You then create a CSamples object to hold them, but a CSamples object contains an array of 100 variables of type CBox, so you call the default constructor another 100 times, once for each element in the array. Of course, the maxBox object will be created by the default copy constructor that is supplied by the compiler.

Class Templates with Multiple Parameters

Using multiple type parameters in a class template is a straightforward extension of the example using a single parameter that you have just seen. You can use each of the type parameters wherever you want in the template definition. For example, you could define a class template with two type parameters:

template<class T1, class T2>
class CExampleClass
{
// Class data members

private:
    T1 m_Value1;
    T2 m_Value2;

  // Rest of the template definition...
};

The types of the two class data members shown will be determined by the types you supply for the parameters when you instantiate an object.

The parameters in a class template aren't limited to types. You can also use parameters that require constants or constant expressions to be substituted in the class definition. In our CSamples template, we arbitrarily defined the m_Values array with 100 elements. You could, however, let the user of the template choose the size of the array when the object is instantiated, by defining the template as:

template <class T, int Size> class CSamples
{
  private:
    T m_Values[Size];                 // Array to store samples
    int m_Free;                       // Index of free location in m_Values

  public:
    // Constructor definition to accept an array of samples
    CSamples(const T values[], int count)
    {
      m_Free = count < Size ? count : Size; // Don't exceed the array

      for(int i = 0; i < m_Free; i++)
        m_Values[i] = values[i];            // Store count number of samples
    }

    // Constructor to accept a single sample
    CSamples(const T& value)
    {
      m_Values[0] = value;             // Store the sample
      m_Free = 1;                      // Next is free
    }
                                       // Default constructor
    CSamples()
    {
      m_Free = 0;                      // Nothing stored, so first is free
    }

    // Function to add a sample
    int Add(const T& value)
    {
      int OK = m_Free < Size;          // Indicates there is a free place
      if(OK)
m_Values[m_Free++] = value;    // OK true, so store the value
      return OK;
    }

    // Function to obtain maximum sample
    T Max() const
    {
      // Set first sample or 0 as maximum
      T theMax = m_Free ? m_Values[0] : 0;

      for(int i = 1; i < m_Free; i++)     // Check all the samples
        if(m_Values[i] > theMax)
          theMax = m_Values[i];           // Store any larger sample
      return theMax;
    }
};

The value supplied for Size when you create an object will replace the appearance of the parameter throughout the template definition. Now, you can declare the CSamples object from the previous example as:

CSamples<CBox, 3> myBoxes(boxes, sizeof boxes/sizeof CBox);

Because you can supply any constant expression for the Size parameter, you could also have written this as:

CSamples<CBox, sizeof boxes/sizeof CBox>
                             myBoxes(boxes, sizeof boxes/sizeof CBox);

The example is a poor use of a template, though — the original version was much more usable. A consequence of making Size a template parameter is that instances of the template that store the same types of objects but have different size parameter values are totally different classes and cannot be mixed. For instance, an object of type CSamples<double, 10> cannot be used in an expression with an object of type CSamples<double, 20>.

You need to be careful with expressions that involve comparison operators when instantiating templates. Look at this statement:

CSamples<aType, x > y ? 10 : 20 > MyType();      // Wrong!

This will not compile correctly because the > preceding y in the expression will be interpreted as a right-angled bracket. Instead, you should write this statement as:

CSamples<aType, (x > y ? 10 : 20) > MyType();    // OK

The parentheses ensure that the expression for the second template argument doesn't get mixed up with the angled brackets.

Templates for Function Objects

A class that defines function objects is typically defined by a template, for the obvious reason that it allows function objects to be defined that will work with a variety of argument types. Here's a template for the Area class that you saw earlier:

template<class T> class Area
{
public:
  T operator()(T length, T width){ return length*width;  }
};

This template will allow you to define function objects to calculate areas with the dimensions of any numeric type. You could define the printArea() function that you saw earlier as a function template:

template<class T> void printArea(T length, T width, Area<T> area)
{  cout << "Area is " <<  area(length, width); }

Now, you can call to the printArea() function like this:

printArea(1.5, 2.5, Area<double>());
printArea(100, 50, Area<int>());

Function objects are applied extensively with the Standard Template Library that you will learn about in Chapter 10, so you will see practical examples of their use in that context.

USING CLASSES

I've touched on most of the basic aspects of defining a native C++ class, so maybe we should look at how a class might be used to solve a problem. The problem has to be simple in order to keep this book down to a reasonable number of pages, so we'll consider problems in which we can use an extended version of the CBox class.

The Idea of a Class Interface

The implementation of an extended CBox class should incorporate the notion of a class interface. You are going to provide a tool kit for anyone wanting to work with CBox objects, so you need to assemble a set of functions that represents the interface to the world of boxes. Because the interface will represent the only way to deal with CBox objects, it needs to be defined to cover adequately the likely things one would want to do with a CBox object, and be implemented, as far as possible, in a manner that protects against misuse or accidental errors.

The first question that you need to consider in designing a class is the nature of the problem you intend to solve, and, from that, determine the kind of functionality you need to provide in the class interface.

Defining the Problem

The principal function of a box is to contain objects of one kind or another, so, in a word, the problem is packaging. We'll attempt to provide a class that eases packaging problems in general and then see how it might be used. We will assume that we'll always be working on packing CBox objects into other CBox objects since, if you want to pack candy in a box, you can always represent each of the pieces of candy as an idealized CBox object. The basic operations that you might want to provide in the CBox class include:

  • Calculate the volume of a CBox. This is a fundamental characteristic of a CBox object, and you have an implementation of this already.

  • Compare the volumes of two CBox objects to determine which is the larger. You probably should support a complete set of comparison operators for CBox objects. You already have a version of the > operator.

  • Compare the volume of a CBox object with a specified value, and vice versa. You also have an implementation of this for the > operator, but you will also need to implement functions supporting the other comparison operators.

  • Add two CBox objects to produce a new CBox object that will contain both the original objects. Thus, the result will be at least the sum of the volumes, but may be larger. You have a version of this already that overloads the + operator.

  • Multiply a CBox object by an integer (and vice versa) to provide a new CBox object that will contain a specified number of the original objects. This is effectively designing a carton.

  • Determine how many CBox objects of a given size can be packed in another CBox object of a given size. This is effectively division, so you could implement this by overloading the / operator.

  • Determine the volume of space remaining in a CBox object after packing it with the maximum number of CBox objects of a given size.

I had better stop right there! There are undoubtedly other functions that would be very useful but, in the interest of saving trees, we'll consider the set to be complete, apart from ancillaries such as accessing dimensions, for example.

Implementing the CBox Class

You really need to consider the degree of error protection that you want to build into the CBox class. The basic class that you defined to illustrate various aspects of classes is a starting point, but you should also consider some points a little more deeply. The constructor is a little weak in that it doesn't ensure that the dimensions for a CBox are valid, so, perhaps, the first thing you should do is to ensure that you always have valid objects. You could redefine the basic class as follows to do this:

class CBox                                 // Class definition at global scope
{
  public:
    // Constructor definition
    explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0)
{
      lv = lv <= 0 ? 1.0 : lv;         // Ensure positive
      wv = wv <= 0 ? 1.0 : wv;         // dimensions for
      hv = hv <= 0 ? 1.0 : hv;         // the object

      m_Length = lv > wv ? lv : wv;    // Ensure that
      m_Width = wv < lv ? wv : lv;     // length >= width
      m_Height = hv;
    }

    // Function to calculate the volume of a box
    double Volume() const
    {
      return m_Length*m_Width*m_Height;
    }

    // Function providing the length of a box
    double GetLength() const { return m_Length; }

    // Function providing the width of a box
    double GetWidth() const { return m_Width; }

    // Function providing the height of a box
    double GetHeight() const { return m_Height; }

  private:
    double m_Length;                   // Length of a box in inches
    double m_Width;                    // Width of a box in inches
    double m_Height;                   // Height of a box in inches
};

The constructor is now secure because any dimension that the user of the class tries to set to a negative number or zero will be set to 1 in the constructor. You might also consider displaying a message for a negative or zero dimension because there is obviously an error when this occurs, and arbitrarily and silently setting a dimension to 1 might not be the best solution.

The default copy constructor is satisfactory for our class, because you have no dynamic memory allocation for data members, and the default assignment operator will also work as you would like. The default destructor also works perfectly well in this case, so you do not need to define it. Perhaps now, you should consider what is required to support comparisons of objects of our class.

Comparing CBox Objects

You should include support for the operators >, >=, ==, <, and <= so that they work with both operands as CBox objects, as well as between a CBox object and a value of type double. You can implement these as ordinary global functions because they don't need to be member functions. You can write the functions that compare the volumes of two CBox objects in terms of the functions that compare the volume of a CBox object with a double value, so let's start with the latter. You can start by repeating the operator>() function that you had before:

// Function for testing if a constant is > a CBox object
bool operator>(const double& value, const CBox& aBox)
{
return value > aBox.Volume();
}

You can now write the operator<() function in a similar way:

// Function for testing if a constant is < CBox object
bool operator<(const double& value, const CBox& aBox)
{
  return value < aBox.Volume();
}

You can code the implementations of the same operators with the arguments reversed in terms of the two functions you have just defined:

// Function for testing if CBox object is > a constant
bool operator>(const CBox& aBox, const double& value)
{ return value < aBox; }

// Function for testing if CBox object is < a constant
int operator<(const CBox& aBox, const double& value)
{ return value > aBox; }

You just use the appropriate overloaded operator function that you wrote before, with the arguments from the call to the new function switched.

The functions implementing the >= and <= operators will be the same as the first two functions, but with the <= operator replacing each use of <, and >= instead of >; there's little point in reproducing them at this stage. The operator==() functions are also very similar:

// Function for testing if constant is == the volume of a CBox object
bool operator==(const double& value, const CBox& aBox)
{
   return value == aBox.Volume();
}

// Function for testing if CBox object is == a constant
bool operator==(const CBox& aBox, const double& value)
{
   return value == aBox;
}

You now have a complete set of comparison operators for CBox objects. Keep in mind that these will also work with expressions, as long as the expressions result in objects of the required type, so you will be able to combine them with the use of other overloaded operators.

Combining CBox Objects

Now, you come to the question of overloading the operators +, *, /, and %. I will take them in order. The add operation that you already have from Ex8_06.cpp has this prototype:

CBox operator+(const CBox& aBox) const;    // Function adding two CBox objects

Although the original implementation of this isn't an ideal solution, let's use it anyway to avoid overcomplicating the class. A better version would need to examine whether the operands had any faces with the same dimensions and, if so, join along those faces, but coding that could get a bit messy. Of course, if this were a practical application, a better add operation could be developed later and substituted for the existing version, and any programs written using the original would still run without change. The separation of the interface to a class from its implementation is crucial to good C++ programming.

Notice that I conveniently forgot the subtraction operator. This is a judicious oversight to avoid the complications inherent in implementing this. If you're really enthusiastic about it, and you think it's a sensible idea, you can give it a try — but you need to decide what to do when the result has a negative volume. If you allow the concept, you need to resolve which box dimension or dimensions are to be negative, and how such a box is to be handled in subsequent operations.

The multiply operation is very easy. It represents the process of creating a box to contain n boxes, where n is the multiplier. The simplest solution would be to take the m_Length and m_Width of the object to be packed and multiply the height by n to get the new CBox object. You can make it a little cleverer by checking whether or not the multiplier is even and, if it is, stack the boxes side by side by doubling the m_Width value and only multiplying the m_Height value by half of n. This mechanism is illustrated in Figure 8-6.

FIGURE 8-6

Figure 8.6. FIGURE 8-6

Of course, you don't need to check which is the larger of the length and width for the new object because the constructor will sort it out automatically. You can write the version of the operator*() function as a member function with the left operand as a CBox object:

// CBox multiply operator this*n
CBox operator*(int n) const
{
  if(n % 2)
    return CBox(m_Length, m_Width, n*m_Height);            // n odd
  else
    return CBox(m_Length, 2.0*m_Width, (n/2)*m_Height);    // n even
}

Here, you use the % operator to determine whether n is even or odd. If n is odd, the value of n % 2 is 1 and the if statement is true. If it's even, n % 2 is 0 and the statement is false.

You can now use the function you have just written in the implementation of the version with the left operand as an integer. You can write this as an ordinary non-member function:

// CBox multiply operator n*aBox
CBox operator*(int n, const CBox& aBox)
{
  return aBox*n;
}

This version of the multiply operation simply reverses the order of the operands so as to use the previous version of the function directly. That completes the set of arithmetic operators for CBox objects that you defined. You can finally look at the two analytical operator functions, operator/() and operator%().

Analyzing CBox Objects

As I have said, the division operation will determine how many CBox objects identical to that specified by the right operand can be contained in the CBox object specified by the left operand. To keep it relatively simple, assume that all the CBox objects are packed the right way up, that is, with the height dimensions vertical. Also assume that they are all packed the same way round, so that their length dimensions are aligned. Without these assumptions, it can get rather complicated.

The problem will then amount to determining how many of the right-operand objects can be placed in a single layer, and then deciding how many layers you can get inside the left-operand CBox.

You can code this as a member function like this:

int operator/(const CBox& aBox) const
{
  int tc1 = 0;        // Temporary for number in horizontal plane this way
  int tc2 = 0;        // Temporary for number in a plane that way

tc1 = static_cast<int>((m_Length / aBox.m_Length))*
         static_cast<int>((m_Width / aBox.m_Width));  // to fit this way
  tc2 = static_cast<int>((m_Length / aBox.m_Width))*
static_cast<int>((m_Width / aBox.m_Length)); // and that way

  //Return best fit
  return static_cast<int>((m_Height/aBox.m_Height)*(tc1>tc2 ? tc1 : tc2));
}

This function first determines how many of the right-operand CBox objects can fit in a layer with their lengths aligned with the length dimension of the left-operand CBox. This is stored in tc1. You then calculate how many can fit in a layer with the lengths of the right-operand CBoxes lying in the width direction of the left-operand CBox. Finally, you multiply the larger of tc1 and tc2 by the number of layers you can pack in, and return that value. This process is illustrated in Figure 8-7.

FIGURE 8-7

Figure 8.7. FIGURE 8-7

Consider two possibilities: fitting bBox into aBox with the length aligned with that of aBox, and then with the length of bBox aligned with the width of aBox. You can see from Figure 8-7 that the best packing results from rotating bBox so that the width divides into the length of aBox.

The other analytical operator function, operator%(), for obtaining the free volume in a packed aBox is easier, because you can use the operator you have just written to implement it. You can write it as an ordinary global function because you don't need access to the private members of the class.

// Operator to return the free volume in a packed box
double operator%(const CBox& aBox, const CBox& bBox)
{
  return aBox.Volume() - ((aBox/bBox)*bBox.Volume());
}

This computation falls out very easily using existing class functions. The result is the volume of the big box, aBox, minus the volume of the bBox boxes that can be stored in it. The number of bBox objects packed into aBox is given by the expression aBox/bBox, which uses the previous overloaded operator. You multiply this by the volume of bBox objects to get the volume to be subtracted from the volume of the large box, aBox.

That completes the class interface. Clearly, there are many more functions that might be required for a production problem solver but, as an interesting working model demonstrating how you can produce a class for solving a particular kind of problem, it will suffice. Now you can go ahead and try it out on a real problem.

ORGANIZING YOUR PROGRAM CODE

In example Ex8_11, you distributed the code among several files for the first time. Not only is this common practice with C++ applications generally, but with Windows programming, it is essential. The sheer volume of code involved in even the simplest program necessitates dividing it into workable chunks.

As I discussed in the previous section, there are basically two kinds of source code files in a C++ program, .h files and .cpp files. This is illustrated in Figure 8-14.

FIGURE 8-14

Figure 8.14. FIGURE 8-14

First, there's the executable code that corresponds to the definitions of the functions that make up the program. Second, there are definitions of various kinds that are necessary for the executable code to compile correctly. These are global constants and variables; data types that include classes, structures, and unions; and function prototypes. The executable source code is stored in files with the extension .cpp, and the definitions are stored in files with the extension .h.

From time to time, you might want to use code from existing files in a new project. In this case, you only have to add the .cpp files to the project, which you can do by using the Project

FIGURE 8-14

In a Windows program, there are other kinds of definitions for the specification of such things as menus and toolbar buttons. These are stored in files with extensions like .rc and .ico. Just like .h files, these do not need to be explicitly added to a project, as they are created and tracked automatically by Visual C++ 2010 when you need them.

Naming Program Files

As I have already said, for classes of any complexity, it's usual to store the class definition in a .h file with a file name based on the class name, and to store the implementation of the function members of the class that are defined outside the class definition in a .cpp file with the same name. On this basis, the definition of our CBox class appeared in a file with the name Box.h. Similarly, the class implementation was stored in the file Box.cpp. We didn't follow this convention in the earlier examples in the chapter because the examples were very short, and it was easier to reference the examples with names derived from the chapter number and the sequence number of the example within the chapter. With programs of any size, though, it becomes essential to structure the code in this way, so it would be a good idea to get into the habit of creating .h and .cpp files to hold your program code from now on.

Segmenting a C++ program into .h and .cpp files is a very convenient approach, as it makes it easy for you to find the definition or implementation of any class, particularly if you're working in a development environment that doesn't have all the tools that Visual C++ provides. As long as you know the class name, you can go directly to the file you want. This isn't a rigid rule, however. It's sometimes useful to group the definitions of a set of closely related classes together in a single file and assemble their implementations similarly. However you choose to structure your files, the Class View still displays all the individual classes, as well as all the members of each class, as you can see in Figure 8-15.

FIGURE 8-15

Figure 8.15. FIGURE 8-15

I adjusted the size of the Class View pane so all the elements in the project are visible. Here, you can see the details of the classes and globals for the last example. As I've mentioned, double-clicking any of the entries in the tree will take you directly to the relevant source code.

NATIVE C++ LIBRARY CLASSES FOR STRINGS

As I mentioned in Chapter 4, the string standard header defines the string and wstring classes that represent character strings. Both are defined in the string header as template classes that are instances of the basic_string<T> class template. The string class is defined as basic_string<char>, and wstring is defined as basic_string<wchar_t>, so the string class represents strings of characters of type char, and wstring represents strings of characters of type wchar_t.

These string types are much easier to use than null-terminated strings and bring with them a whole range of powerful functions. Because string and wstring are both instances of the same template, basic_string<T>, they provide the same functionality, so I'll only discuss the features and use in the context of the string type. The wstring type will work just the same, except that the strings contain Unicode character codes and you must use the L prefix for string literals in your code.

Creating String Objects

Creating string objects is very easy but you have a lot of choices as to how you do it. First, you can create and initialize a string object like this:

string sentence = "This sentence is false.";

The sentence object will be initialized with the string literal that appears to the right of the assignment operator. A string object has no terminating null character, so the string length is the number of characters in the string, 23 in this instance. You can discover the length of the string encapsulated by a string object at any time by calling its length() member function. For example:

cout << "The string is of length " << sentence.length() << endl;

Executing the statement produces the output:

The string is of length 23

Incidentally, you can output a string object to stdout in the same way as any other variable:

cout << sentence << endl;

This displays the sentence string on a line by itself. You can also read a character string into a string object like this:

cin >> sentence;

However, reading from stdin in this way ignores leading whitespace until a non-whitespace character is found, and also terminates input when you enter a space following one or more non-whitespace characters. You will often want to read text into a string object that includes spaces and may span several lines. In this case, the getline()function template that is defined in the string header is much more convenient. For example:

getline(cin, sentence, '*'),

This function template is specifically for reading data from a stream into a string or wstring object. The first argument is the stream that is the source of input — it doesn't have to be cin; the second argument is the object that is to receive the input; and the third argument is the character that terminates reading. Here, I have specified the terminating character as '*', so this statement will read text from cin, including spaces, into sentence, until the end of input is indicated by an asterisk being read from the input stream.

Of course, you can also use functional notation to initialize a string object:

string sentence("This sentence is false.");

If you don't specify an initial string literal when you create a string object, the object will contain an empty string:

string astring;                        // Create an empty string

Calling the length() of the string astring will result in zero.

Another possibility is to initialize a string object with a single character repeated a specified number of times:

string bees(7, 'b'),                   // String is "bbbbbbb"

The first argument to the constructor is the number of repetitions of the character specified by the second argument.

Finally, you can initialize a string object with all or part of another string object. Here's an example of using another string object as an initializer:

string letters(bees);

The letters object will be initialized with the string contained in bees.

To select part of a string object as initializer, you call the string constructor with three arguments, the first being the string object that is the source of the initializing string, the second being the index position of the first character to be selected, and the third argument being the number of characters to be selected. Here's an example:

string sentence("This sentence is false.");
string part(sentence, 5, 11);

The part object will be initialized with 11 characters from sentence beginning with the sixth character (the first character is at index position 0). Thus, part will contain the string "sentence is".

Of course, you can create arrays of string objects and initialize them using the usual notation. For example:

string animals[] = { "dog", "cat", "horse", "donkey", "lion"};

This creates an array of string objects that has five elements initialized with the string literals between the braces.

Concatenating Strings

Perhaps the most common operation with strings is joining two strings to form a single string. You can use the + operator to concatenate two string objects or a string object and a string literal. Here are some examples:

string sentence1("This sentence is false.");
string sentence2("Therefore the sentence above must be true!");
string combined;                       // Create an empty string
sentence1 = sentence1 + "
";          // Append string containing newline
combined = sentence1 + sentence2;      // Join two strings
cout << combined << endl;              // Output the result

Executing these statements will result in the following output:

This sentence is false.
Therefore the sentence above must be true!

The first three statements create string objects. The next statement appends the string literal " " to sentence1 and stores the result in sentence1. The next statement joins sentence1 and sentence2 and stores the result in combined. The last statement outputs the string combined.

String concatenation using the + operator is possible because the string class implements operator+(). This implies that one of the operands must be a string object, so you can't use the + operator to join two string literals. Keep in mind that each time you use the + operator to join two strings, you are creating a new string object, which involves a certain amount of overhead. You'll see in the next section how you can modify and extend an existing string object, and this may be a more efficient alternative in some cases, because it does not involve creating new objects.

You can also use the + operator to join a character to a string object, so you could have written the fourth statement in the previous code fragment as:

sentence1 = sentence1 + '
';          // Append newline character to string

The string class also implements operator+=() such that the right operand can be a string literal, a string object, or a single character. You could write the previous statement as:

sentence1 += '
';

or

sentence1 += "
";

There is a difference between using the += operator and using the + operator. As I said, the + operator creates a new string object containing the combined string. The += operator appends the string or character that is the right operand to the string object that is the left operand, so the string object is modified directly and no new object is created.

Let's exercise some of what I have described in an example.

Accessing and Modifying Strings

You can access any character in a string object to read it or overwrite it by using the subscript operator, []. Here's an example:

string sentence("Too many cooks spoil the broth.");
for(size_t i = 0; i < sentence.length(); i++)
{
  if(' ' == sentence[i])
    sentence[i] = '*';
}

This just inspects each character in the sentence string in turn to see if it is a space, and if it is, replaces the character with an asterisk.

You can use the at() member function to achieve the same result as the [] operator:

string sentence("Too many cooks spoil the broth.");
for(size_t i = 0; i < sentence.length(); i++)
{
  if(' ' == sentence.at(i))
    sentence.at(i) = '*';
}

This does exactly the same as the previous fragment, so what's the difference between using [] and using at()? Well, subscripting is faster than using the at() function, but the downside is the validity of the index is not checked. If the index is out of range, the result of using the subscript operator is undefined. The at() function, on the other hand, is a bit slower, but it does check the index, and if it is not valid, the function will throw an out_of_range exception. You would use the at() function when there is the possibility of the index value being out of range, and in this situation, you should put the code in a try block and handle the exception appropriately. If you are sure index out of range conditions cannot arise, then use the [] operator.

You can extract a part of an existing string object as a new string object. For example:

string sentence("Too many cooks spoil the broth.");
string substring = sentence.substr(4, 10);           // Extracts "many cooks"

The first argument to the substr() function is the first character of the substring to be extracted, and the second argument is the count of the number of characters in the substring.

By using the append() function for a string object, you can add one or more characters to the end of the string. This function comes in several versions; there are versions that append one or more of a given character, a string literal, or a string object to the object for which the function is called. For example:

string phrase("The higher");
string word("fewer");
phrase.append(1, ' '),                 // Append one space
phrase.append("the ");                 // Append a string literal
phrase.append(word);                   // Append a string object
phrase.append(2, '!'),                 // Append two exclamation marks

After executing this sequence, phrase will have been modified to "The higher the fewer!!". With the version of append() with two arguments, the first argument is the count of the number of times the character specified by the second argument is to be appended. When you call append(), the function returns a reference to the object for which it was called, so you could write the four append() calls above in a single statement:

phrase.append(1, ' ').append("the ").append(word).append(2, '!'),

You can also use append() to append part of a string literal or part of a string object to an existing string:

string phrase("The more the merrier.");
string query("Any");
query.append(phrase, 3, 5).append(1, '?'),

The result of executing these statements is that query will contain the string "Any more?". In the last statement, the first call to the append() function has three arguments:

  • The first argument, phrase, is the string object from which characters are to be extracted and appended to query.

  • The second argument, 3, is the index position of the first character to be extracted.

  • The third argument, 5, is the count of the total number of characters to be appended.

Thus, the substring " more" is appended to query by this call. The second call for the append() function appends a question mark to query.

When you want to append a single character to a string object, you could use the push_back() function as an alternative to append(). Here's how you would use that:

query.push_back('*'),

This appends an asterisk character to the end of the query string.

Sometimes, adding characters to the end of a string just isn't enough. There will be occasions when you want to insert one or more characters at some position in the interior of a string. The various flavors of the insert() function will do that for you:

string saying("A horse");
string word("blind");
string sentence("He is as good as gold.");
string phrase("a wink too far");
saying.insert(1, " ");                  // Insert a space character
saying.insert(2, word);                 // Insert a string object
saying.insert(2, "nodding", 3);         // Insert 3 characters of a string literal
saying.insert(5, sentence, 2, 15);      // Insert part of a string at position 5
saying.insert(20, phrase, 0, 9);        // Insert part of a string at position 20
saying.insert(29, " ").insert(30, "a poor do", 0, 2);

I'm sure you'll be interested to know that after executing the statements above, saying will contain the string "A nod is as good as a wink to a blind horse". The parameters to the various versions of insert()are:

FUNCTION PROTOTYPE

DESCRIPTION

string& insert(
  size_t index,
const char* pstring)

Inserts the null-terminated string pstring at position index.

string& insert(
  size_t index,
  const string& astring)

Inserts the string object astring at position index.

string& insert(
  size_t index,
  const char* pstring,
  size_t count)

Inserts the first count characters from the null-terminated string pstring at position index.

string& insert(
  size_t index,
  size_t count,
  char ch)

Inserts count copies of the character ch at position index.

string& insert(
  size_t index,
  const string& astring,
  size_t start,
  size_t count)

Inserts count characters from the string object astring, beginning with the character at position start; the substring is inserted at position index.

In each of these versions of insert(), a reference to the string object for which the function is called is returned; this allows you to chain calls together, as in the last statement in the code fragment.

This is not the complete set of insert() functions, but you can do everything you need with those in the table. The other versions use iterators as arguments, and you'll learn about iterators in Chapter 10.

You can interchange the strings encapsulated by two string objects by calling the swap() member function. For example:

string phrase("The more the merrier.");
string query("Any");
query.swap(phrase);

This results in query containing the string "The more the merrier." and phrase containing the string "Any". Of course, executing phrase.swap(query) would have the same effect.

If you need to convert a string object to a null-terminated string, the c_str() function will do this. For example:

string phrase("The higher the fewer");
const char *pstring = phrase.c_str();

The c_str() function returns a pointer to a null-terminated string with the same contents as the string object.

You can also obtain the contents of a string object as an array of elements of type char by calling the data() member function. Note that the array contains just the characters from the string object without a terminating null.

You can replace part of a string object by calling its replace() member function. This also comes in several versions, as the following table shows.

FUNCTION PROTOTYPE

DESCRIPTION

string& replace(
  size_t index,
  size_t count,
  const char* pstring)

Replaces count characters, starting at position index, with the first count characters from pstring.

string& replace(
  size_t index,
  size_t count,
  const string& astring)

Replaces count characters, starting at position index, with the first count characters from astring.

string& replace(
  size_t index,
  size_t count1,
  const char* pstring,
  size_t count2)

Replaces count1 characters, starting at position index, with up to count2 characters from pstring. This allows the replacement substring to be longer or shorter than the substring that is replaced.

string& replace(
  size_t index1,
  size_t count1,
  const string& astring,
  size_t index2,
  size_t count2)

Replaces count1 characters, starting at position index1, with count2 characters from astring, starting at position index2.

string& replace(
  size_t index,
  size_t count1,
  size_t count2,
  char ch)

Replaces count1 characters, starting at index, with count2 occurrences of the character ch.

In each case, a reference to the string object for which the function is called is returned.

Here's an example:

string proverb("A nod is as good as a wink to a blind horse");
string sentence("It's bath time!");
proverb.replace(38, 5, sentence, 5, 3);

This fragment uses the fifth version of the replace() function from the preceding table to substitute "bat" in place of "horse" in the string proverb.

Comparing Strings

You have a full complement of operators for comparing two string objects or comparing a string object with a string literal. Operator overloading has been implemented in the string class for the following operators:

==   !=   <   <=   >   >=

Here's an example of the use of these operators:

string dog1("St Bernard");
string dog2("Tibetan Mastiff");
if(dog1 < dog2)
  cout << "dog2 comes first!" << endl;
else if(dog1 > dog2)
  cout << "dog1 comes first!" << endl;

When you compare two strings, corresponding characters are compared until a pair of characters is found that differ, or the end of one or both strings is reached. When two corresponding characters are found to be different, the values of the character codes determine which string is less than the other. If no character pairs are found to be different, the string with fewer characters is less than the other string. Two strings will be equal if they contain the same number of characters and corresponding characters are identical.

Searching Strings

You have four versions of the find() function that search a string object for a given character or substring, and they are described in the following table. All the find() functions are defined as being const.

FUNCTION

DESCRIPTION

size_t find(
  char ch,
  size_t offset=0)

Searches a string object for the character ch starting at index position offset. You can omit the second argument, in which case, the default value is 0.

size_t find(
  const char* pstr,
  size_t offset=0)

Searches a string object for the null-terminated string pstr, starting at index position offset. You can omit the second argument, in which case, the default value is 0.

size_t find(
  const char* pstr,
  size_t offset,
  size_t count)

Searches a string object for the first count characters of the null-terminated string pstr, starting at index position offset.

size_t find(
  const string& str,
  size_t offset=0)

Searches a string object for the string object str, starting at index position offset. You can omit the second argument, in which case, the default value is 0.

In each case, the find() function returns the index position where the character or first character of the substring was found. The function returns the value string::npos if the item was not found. This latter value is a constant defined in the string class that represents an illegal position in a string object; it is used generally to signal a search failure.

Here's a fragment showing some of the ways you might use the find() function:

string phrase("So near and yet so far");
string str("So near");
cout << phrase.find(str) << endl;           // Outputs 0
cout << phrase.find("so far") << endl;      // Outputs 16
cout << phrase.find("so near") << endl;     // Outputs string::npos = 4294967295

The value of string::npos can vary with different C++ compiler implementations, so to test for it, you should always use string::npos and not the explicit value.

Here's another example that scans the same string repeatedly, searching for occurrences of a particular substring:

string str( "Smith, where Jones had had "had had", "had had" had."
            " "Had had" had had the examiners' approval.");
string substr("had");

cout << "The string to be searched is:"
     << endl << str << endl;
size_t offset(0);
size_t count(0);
size_t increment(substr.length());

while(true)
{
  offset = str.find(substr, offset);
  if(string::npos == offset)
    break;
  offset += increment;
  ++count;
}
cout << endl << " The string "" << substr
     << "" was found " << count << " times in the string above."
     << endl;

Here, you search the string str to see how many times "had" appears. The search is done in the while loop, where offset records the position found, and is also used as the start position for the search. The search starts at index position 0, the start of the string, and each time the substring is found, the new starting position for the next search is set to the found position plus the length of the substring. This ensures that the substring that was found is bypassed. Every time the substring is found, count is incremented. If find() returns string::npos, then the substring was not found and the search ends. Executing this fragment produces the output:

The string to be searched is:
Smith, where Jones had had "had had", "had had" had. "Had had" had had the
examiners' approval.

 The string "had" was found 10 times in the string above.

Of course, "Had" is not a match for the substring "had," so 10 is the correct result.

The find_first_of() and find_last_of() member functions search a string object for an occurrence of any character from a given set. You could search a string to find spaces or punctuation characters, for example, which would allow you to break a string up into individual words. Both functions come in several flavors, as the following table shows. All functions in the table are defined as const and return a value of type size_t.

FUNCTION

DESCRIPTION

find_first_of(
  char ch,
  size_t offset = 0)

Searches a string object for the first occurrence of the character, ch, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is 0.

find_first_of(
  const char* pstr,
  size_t offset = 0)

Searches a string object for the first occurrence of any character in the null-terminated string, pstr, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is 0.

find_first_of(
  const char* pstr,
  size_t offset,
  size_t count)

Searches a string object for the first occurrence of any character in the first count characters of the null-terminated string, pstr, starting at position offset, and returns the index position where the character is found as a value of type size_t.

find_first_of(
  const string& str,
  size_t offset = 0)

Searches a string object for the first occurrence of any character in the string, str, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is 0.

find_last_of(
  char ch,
  size_t offset=npos)

Searches backward through a string object for the last occurrence of the character, ch, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is npos, which is the end of the string.

find_last_of(
  const char* pstr,
  size_t offset=npos)

Searches backward through a string object for the last occurrence of any character in the null-terminated string, pstr, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is npos, which is the end of the string.

find_last_of(
  const char* pstr,
  size_t offset,
  size_t count)

Searches backward through a string object for the last occurrence of any of the first count characters in the null-terminated string, pstr, starting at position offset, and returns the index position where the character is found as a value of type size_t.

find_last_of(
  const string& str,
  size_t offset=npos)

Searches backward through a string object for the last occurrence of any character in the string, str, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is npos, which is the end of the string.

With all versions of the find_first_of() and find_last_of() functions, string::npos will be returned if no matching character is found.

With the same string as the last fragment, you could see what the find_last_of() function does with the same search string "had."

size_t count(0);
size_t offset(string::npos);
while(true)
{
  offset = str.find_last_of(substr, offset);
  if(string::npos == offset)
    break;
  --offset;
  ++count;
}
cout << endl << " Characters from the string "" << substr
     << "" were found " << count << " times in the string above."
     << endl;

This time, you are searching backward starting at index position string::npos, the end of the string, because this is the default starting position. The output from this fragment is:

The string to be searched is:
Smith, where Jones had had "had had", "had had" had. "Had had" had had
the examiners' approval.

 Characters from the string "had" were found 38 times in the string above.

The result should not be a surprise. Remember, you are searching for occurrences of any of the characters in "had" in the string str. There are 32 in the "Had" and "had" words, and 6 in the remaining words. Because you are searching backward through the string, you decrement offset within the loop when you find a character.

The last set of search facilities are versions of the find_first_not_of() and find_last_not_of() functions. All of the functions in the following table are const and return a value of type size_t.

FUNCTION

DESCRIPTION

find_first_not_of(
  char ch,
  size_t offset = 0)

Searches a string object for the first occurrence of a character that is not the character, ch, starting at position offset. The function returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is 0.

find_first_not_of(
  const char* pstr,
  size_t offset = 0)

Searches a string object for the first occurrence of a character that is not in the null-terminated string, pstr, starting at position offset, and returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is 0.

find_first_ not_of(
  char* pstr,
  size_t offset,
  size_t count)

Searches a string object for the first occurrence of a character that is not in the first count characters of the null-terminated string, pstring, starting at position offset. The function returns the index position where the character is found as a value of type size_t.

find_first_not_of(
  const string& str,
  size_t offset = 0)

Searches a string object for the first occurrence of any character that is not in the string, pstr, starting at position offset. The function returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is 0.

find_last_not_of(
  char ch,
  size_t offset=npos)

Searches backward through a string object for the last occurrence of a character that is not the character, ch, starting at position offset. The index position where the character is found is returned as a value of type size_t. If you omit the second argument, the default value of offset is npos, which is the end of the string.

find_last_not_of(
  const char* pstr,
  size_t offset=npos)

Searches backward through a string object for the last occurrence of any character that is not in the null-terminated string, pstr, starting at position offset. The index position where the character is found is returned as a value of type size_t. If you omit the second argument, the default value of offset is npos, which is the end of the string.

find_last_not_of(
  const char* pstr,
  size_t offset,
  size_t count)

Searches backward through a string object for the last occurrence of a character that is not among the first count characters in the null-terminated string, pstr, starting at position offset. The function returns the index position where the character is found as a value of type size_t.

find_last_not_of(
  const string& str,
  size_t offset=npos)

Searches backward through a string object for the last occurrence of any character not in the string, str, starting at position offset. The function returns the index position where the character is found as a value of type size_t. If you omit the second argument, the default value of offset is npos, which is the end of the string.

As with previous search functions, string::npos will be returned if the search does not find a character. These functions have many uses, typically finding tokens in a string that may be separated by characters of various kinds. For example, text consists of words separated by spaces and punctuation characters, so you could use these functions to find the words in a block of text. Let's see that working in an example.

C++/CLI PROGRAMMING

While you can define a destructor in a reference class in the same way as you do for native C++ classes, most of the time it is not necessary. However, I'll return to the topic of destructors for reference classes in the next chapter. You can also call delete for a handle to a reference class, but again, this is not normally necessary as the garbage collector will delete unwanted objects automatically.

C++/CLI classes support overloading of operators, but there are some differences from operator overloading in native classes that you need to explore. A couple of differences you have already heard about. You'll probably recall that you must not overload the assignment operator in your value classes because the process for the assignment of one value class object to another of the same type is already defined to be member-by-member copying, and you cannot change this. I also mentioned that, unlike native classes, a ref class does not have a default assignment operator — if you want the assignment operator to work with your ref class objects, then you must implement the appropriate function. Another difference from native C++ classes is that functions that implement operator overloading in C++/CLI classes can be static members of a class as well as instance members. This means that you have the option of implementing binary operators in C++/CLI classes with static member functions with two parameters in addition to the possibilities you have seen in the context of native C++ for operator functions as instance functions with one parameter or non-member functions with two parameters. Similarly, in C++/CLI, you have the additional possibility to implement a prefix unary operator as a static member function with no parameters. Finally, although in native C++ you can overload the new operator, you cannot overload the gcnew operator in a C++/CLI class.

Let's look into some of the specifics, starting with value classes.

Overloading Operators in Value Classes

Let's define a class to represent a length in feet and inches and use that as a base for demonstrating how you can implement operator overloading for a value class. Addition seems like a good place to start, so here's the Length value class, complete with the addition operator function:

value class Length
{
private:
  int feet;                            // Feet component
  int inches;                          // Inches component

public:
  static initonly int inchesPerFoot = 12;

  // Constructor
  Length(int ft, int ins) : feet(ft), inches(ins){ }

  // A length as a string
  virtual String^ ToString() override
  {
    return feet.ToString() + (1 == feet ? L" foot " : L" feet ") +
           inches + (1 == inches ? L" inch" : L" inches");
  }

  // Addition operator
  Length operator+(Length len)
  {
    int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet);
    return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
  }
};

The constant, inchesPerFoot, is static, so it will be directly available to static and non-static function members of the class. Declaring inchesPerFoot as initonly means that it cannot be modified, so it can be a public member of the class. There's a ToString() function override defined for the class, so you can write Length objects to the command line using the Console::WriteLine() function. The operator+() function implementation is very simple. The function returns a new Length object produced by combining the feet and inches component for the current object and the parameter, len. The calculation is done by combining the two lengths in inches and then computing the arguments to the Length class constructor for the new object from the value for the combined lengths in inches.

The following code fragment would exercise the new operator function for addition:

Length len1 = Length(6, 9);
Length len2 = Length(7, 8);
Console::WriteLine(L"{0} plus {1} is {2}", len1, len2, len1+len2);

The last argument to the WriteLine() function is the sum of two Length objects, so this will invoke the operator+() function. The result will be a new Length object for which the compiler will arrange to call the ToString() function, so the last statement is really the following:

Console::WriteLine(L"{0} plus {1} is {2}", len1, len2,
                                          len1.operator+(len2).ToString());

The execution of the code fragment will result in the following output:

6 feet 9 inches plus 7 feet 8 inches is 14 feet 5 inches

Of course, you could define the operator+() function as a static member of the Length class, like this:

static Length operator+(Length len1, Length len2)
{
  int inchTotal = len1.inches+len2.inches+inchesPerFoot*(len1.feet+len2.feet);
  return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
}

The parameters are the two Length objects to be added together to produce a new Length object. Because this is a static member of the class, the operator+() function is fully entitled to access the private members, feet and inches, of both the Length objects passed as arguments. Friend functions are not allowed in C++/CLI classes, and an external function would not have access to private members of the class, so you have no other possibilities for implementing the addition operator.

Because you are not working with areas resulting from the product of two Length objects, it really only makes sense to provide for multiplying a Length object by a numerical value. You can implement multiply operator overloading as a static member of the class, but let's define the function outside the class. The class would look like this:

value class Length
{
private:
  int feet;
  int inches;

public:
  static initonly int inchesPerFoot = 12;

  // Constructor
  Length(int ft, int ins) : feet(ft), inches(ins){ }

  // A length as a string
virtual String^ ToString() override
  {
    return feet.ToString() + (1 == feet ? L" foot " : L" feet ") +
           inches + (1 == inches ? L" inch" : L" inches");
  }

  // Addition operator
  Length operator+(Length len)
  {
    int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet);
    return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
  }

  static Length operator*(double x, Length len); // Pre-multiply by a double value
  static Length operator*(Length len, double x); // Post-multiply by a double value
};

The new function declarations in the class provide for overloaded * operator functions to pre- and post-multiply a Length object by a value of type double. The definition of the operator*() function outside the class for pre-multiplication would be:

Length Length::operator *(double x, Length len)
{
  int ins = safe_cast<int>(x*len.inches +x*len.feet*inchesPerFoot);
  return Length(ins/12, ins %12);
}

The post-multiplication version can now be implemented in terms of this:

Length Length::operator *(Length len, double x)
{ return operator*(x, len);  }

This just calls the pre-multiply version with the arguments reversed. You could exercise these functions with the following fragment:

double factor = 2.5;
Console::WriteLine(L"{0} times {1} is {2}", factor, len2, factor*len2);
Console::WriteLine(L"{1} times {0} is {2}", factor, len2, len2*factor);

Both lines of output from this code fragment should reflect the same result from multiplication — 19 feet, 2 inches. The argument expression factor*len2 is equivalent to:

Length::operator*(factor, len2).ToString()

The result of calling the static operator*() function is a new Length object, and the ToString() function for that is called to produce the argument to the WriteLine() function. The expression len2*factor is similar but calls the operator*() function that has the parameters reversed. Although the operator*() functions have been written to deal with a multiplier of type double, they will also work with integers. The compiler will automatically promote an integer value to type double when you use it in an expression such as 12*(len1+len2).

We could expand a little further on overloaded operators in the Length class with a working example.

Overloading the Increment and Decrement Operators

Overloading the increment and decrement operators is simpler in C++/CLI than in native C++. As long as you implement the operator function as a static class member, the same function will serve as both the prefix and postfix operator functions. Here's how you could implement the increment operator for the Length class:

value class Length
{
public:
  // Code as before...

  // Overloaded increment operator function - increment by 1 inch
  static Length operator++(Length len)
  {
    ++len.inches;
    len.feet += len.inches/len.inchesPerFoot;
    len.inches %= len.inchesPerFoot;
    return len;
  }
};

This implementation of the operator++() function increments a length by 1 inch. The following code would exercise the function:

Length len = Length(1, 11);            // 1 foot 11 inches
Console::WriteLine(len++);
Console::WriteLine(++len);

Executing this fragment will produce the output:

1 foot 11 inches
2 feet 1 inch

Thus, the prefix and postfix increment operations are working as they should using a single operator function in the Length class. This occurs because the compiler is able to determine whether to use the value of the operand in a surrounding expression before or after the operand has been incremented, and compile the code accordingly.

Overloading Operators in Reference Classes

Overloading operators in a reference class is essentially the same as overloading operators in a value class, the primary difference being that parameters and return values are typically handles. Let's see how the Length class looks implemented as a reference class, then you'll be able to compare the two versions.

Implementing the Assignment Operator for Reference Types

There are relatively few circumstances where you will need to implement the assignment operator for a reference type because you will typically use handles to refer to objects, and the need for a copy constructor does not arise. However, if you use the Standard Template Library for the CLR that you will meet in Chapter 10, in some situations, you will need to implement the assignment operator, and the compiler will never supply one by default. The form of the function to overload the assignment operator in a reference class is very simple, and is easily understood if you look at an example. Here's how the assignment operator would look for the Length class, for instance:

Length% operator=(const Length% len)
{
  if(this != %len)
  {
    feet = len.feet;
    inches = len.inches;
  }
  return *this;
}

The function parameter is const because it will not be changed, and if you don't declare it as const, the argument will be passed by value and will cause the copy constructor to be called. The return type is also a reference because you always return the object pointed to by this. The if statement checks whether or not the argument and the current object are identical, and if they are, the function just returns *this, which will be the current object. If they are not, you copy each of the data members of len to the current object before returning it.

SUMMARY

In this chapter, you have learned the basics of how you can define classes and how you create and use class objects. You have also learned about how you can overload operators in a class to allow the operators to be applied to class objects.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC

CONCEPT

Destructors

Objects are destroyed using functions called destructors. It is essential to define a destructor in native C++ classes to destroy objects which contain members that are allocated on the heap, because the default constructor will not do this.

The default copy constructor

The compiler will supply a default copy constructor for a native C++ class if you do not define one. The default copy constructor will not deal correctly with objects of classes that have data members allocated on the free store.

Defining a copy constructor

When you define your own copy constructor in a native C++ class, you must use a reference parameter.

The copy constructor in value classes

You must not define a copy constructor in value classes; copies of value class objects are always created by copying fields.

The copy constructor in reference classes

No default copy constructor is supplied for a reference class, although you can define your own when this is necessary.

The assignment operator in native C++ classes

If you do not define an assignment operator for your native C++ class, the compiler will supply a default version. As with the copy constructor, the default assignment operator will not work correctly with classes that have data members allocated on the free store.

The assignment operator in value classes

You must not define the assignment operator in a value class. Assignment of value class objects is always done by copying fields.

The assignment operator in reference classes

A default assignment operator is not provided for reference classes, but you can define your own assignment operator function when necessary.

Native C++ classes that allocate memory on the heap.

It is essential that you provide a destructor, a copy constructor, and an assignment operator for native C++ classes that have members allocated by new.

Unions

A union is a mechanism that allows two or more variables to occupy the same location in memory.

Fields in C++/CLI classes

C++/CLI classes can contain literal fields that define constants within a class. They can also contain initonly fields that cannot be modified once they have been initialized.

Operator overloading

Most basic operators can be overloaded to provide actions specific to objects of a class. You should only implement operator functions for your classes that are consistent with the normal interpretation of the basic operators.

The string class

The string class in the standard library for native C++ provides a powerful and superior way to process strings in your programs.

Class templates

A class template is a pattern that you can use to create classes with the same structure, but which support different data types.

Class template parameters

You can define a class template that has multiple parameters, including parameters that can assume constant values rather than types.

Organizing your code

You should put definitions for your programs in .h files, and executable code — function definitions — in .cpp files. You can then incorporate .h files into your .cpp files by using #include directives.

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

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