The Include Guard

The first new feature in this header file doesn't have anything to do with adding new functionality. Instead, it is a means of preventing problems if we #include this header twice. I'm referring to the two lines at the very beginning of the file and the last line at the end of the file. The first of these lines,

#ifndef XSTRING_H

uses a preprocessor directive called #ifndef (short for “if not defined”) to determine whether we've already defined a preprocessor symbol called XSTRING_H. If we have already defined this symbol, the compiler will ignore the rest of the file until it sees a #endif (which in this case is at the end of the file).

The next line,

#define XSTRING_H

defines the same preprocessor symbol, XSTRING_H, that we tested for in the previous line. Finally, the last line of the file,

#endif

ends the scope of the #ifndef directive. This whole mechanism is generally referred to as an include guard.

Susan: I don't get it. What is a preprocessor directive? For that matter, what is a preprocessor?

Steve: The preprocessor used to be a separate program that was executed before the compiler itself, to prepare the source code for processing by the compiler. Nowadays, the preprocessor is almost always part of the compiler, but it is still logically distinct. A preprocessor directive is a command to the preprocessor to manipulate the source code in some way.

Susan: Why do we need the preprocessor anyway?

Steve: We don't need it very much anymore. About the only functions it still serves are the processing of included header files (via the #include preprocessor directive) and the creation of the “include guard”.

Susan: About the preprocessor symbol: Why would you want several things all equal to (for example) 123?

Steve: Because that makes the program easier to read than if you just said 123 everywhere you needed to use such a value. Giving a name to a number is now most commonly done via the const construct in C++, which replaces most of the old uses of preprocessor symbols, but we still need them to implement include guards so that we can prevent the C++ compiler itself from seeing the definition of a class more than once.

What is the point of all this? To solve a problem in writing large C++ programs: the possibility that we might #include the same header file more than once in the same source code file. This can happen because a source code file often uses #include to gain access to a number of interface definitions stored in several header files, more than one of which may use a common header file (like xstring.h). If this were to happen without precautions such as an include guard, we would get an error when we tried to compile our program. The error message would say that we had defined the same class twice, which is not allowed. Therefore, any header file that might be used in a number of places should use an include guard to prevent such errors. Susan had some questions about this notion and why it should be needed in the first place.

Susan: Why should it be illegal to define the same class twice?

Steve: If we define the same class twice, which definition should the compiler use? The first one or the second one?

Susan: I see how that might cause a problem, but what if the two definitions are exactly the same? Why would the compiler care then?

Steve: For the compiler to handle that situation, it would have to keep track of every definition it sees for a class rather than just one. Because it's almost always an error to try to define the same class more than once, there's no reason to add that extra complexity to the compiler when we can prevent the problem in the first place.

Assuming that I've convinced you of the value of include guards, how do they work? Well, the #ifndef directive checks to see if a specific preprocessor symbol, in this case XSTRING_H, has already been defined. If it has, then the rest of the #include file is essentially ignored. Let's suppose that XSTRING_H hasn't been defined yet. In that case, we define that symbol in the next line and then allow the compiler to process the rest of the file.

So far this works exactly as it would if we hadn't added the include guard. But suppose that later, during the compilation of the same source file, another header file #includes xstring.h again. In that case, the symbol XSTRING_H would already be defined (because we defined it on the first access to xstring.h). Therefore, the #ifndef would cause the compiler to skip the rest of the header file, preventing the xstring class from being redefined and causing an error.

Of course, the choice of the preprocessor symbol to be defined is more or less arbitrary, but there is a convention in C and C++ that the symbol should be derived from the name of the header file. This is intended to reduce the likelihood of two header files using the same preprocessor symbol in their include guards. If that happened and if both of these header files were #included in the same source file, the definitions in the second one to be #included would be ignored during compilation because the preprocessor symbol used by its include guard would already be defined. To prevent such problems, I'm following the (commonly used) convention of defining a preprocessor symbol whose name is a capitalized version of the header file's name, with the period changed to an underscore to make it a legal preprocessor symbol name. If everyone working on a project follows this convention (or a similar one), the likelihood of trouble will be minimized.

The Constructors for the xstring class

You'll notice that we have declared six constructors for this new class. Let's go through them and see why we need each one and how it is implemented.

First, we have the default constructor, xstring::xstring(). As always, this constructor is designed to create a new object of its class when no initial data is specified for that object. The implementation of this function, which is shown in Figure 12.2, is fairly simple: it merely initializes its base class part, string, using the default constructor for that class.

Figure 12.2. The default constructor for the xstring class (from codexstring.h)
xstring::xstring()
: string()
{
}

Next, we have the copy constructor, xstring::xstring(const xstring& Str), which is shown in Figure 12.3. This one isn't much more complicated on the surface; it just initializes its base class part to be equal to the existing xstring object that we are copying from.

Figure 12.3. The copy constructor for the xstring class (from codexstring.h)
xstring::xstring(const xstring& Str)
: string(Str)
{
}

But there is a subtle issue here that needs examination: how can we initialize a string from an xstring, as we are doing here?

We can do this because we have declared xstring as publicly derived from string. Normally, this feature of C++ wouldn't be desirable, as any added data members of the xstring class would be lost in the assignment; this problem, which we have discussed before, is called slicing. However, in this case that's not a problem, because we don't have any added data members in xstring, so we don't have to worry about their being lost.

Now we come to a constructor that's a little more interesting: xstring::xstring(const string& Str), shown in Figure 12.4.[3]

[3] If you're paying very close attention here, you'll notice that the declaration in the header file specifies std::string, not simply string, as the definition in this figure does. The reason we don't have to specify std::string in the definition is that we have included the statement using std::string; in the implementation file so that we don't have to repeat the std:: qualifier throughout this file. I didn't include that using statement in the header file, for reasons explained under the heading “Implementing operator <” on page 510 in Chapter 8, so we need the std:: qualifier there.

Figure 12.4. Another constructor for the xstring class (from codexstring.h)
xstring::xstring(const string& Str)
: string(Str)
{
}

What does this do, and why do we need it?

Automatic Conversion from the Standard string class

This constructor will automatically convert an object of the standard string class to one of our xstring objects, in much the same way that the standard copy constructor creates a new object of the same type as an existing one. In this case, however, the new object being created is an xstring, with the same contents as the standard library string that we are copying from.

That much should be reasonably clear. But it will take some more explanation to explain why we need it. The reason is that otherwise we will not be able to use all of the normal facilities of the standard string class in a natural manner. Since the standard library doesn't know about our xstring class, standard library functions will never return a value of that type; instead, they will return standard library strings. So if we want to be able to tap into all of the functionality of the standard string class, we have to convert a returned std::string into one of our xstrings.

Let's make this a bit more concrete with an example. In this example, we will use a new feature of the standard library string class: concatenation, which is just a fancy word for “adding one string onto the end of another one”. For example, if we have someone's first and last names as separate strings, it might be handy to be able to tack the last name to the end of the first name so we can store the entire name as one string. While we could use stringstreams, that is a very inefficient and clumsy way to accomplish a common operation.

Concatenation is a common enough operation that a convention has been developed to use the + sign to indicate it. This symbol is also used in languages such as Java and Basic for the same operation, so C++ isn't too unusual in this regard. But exactly how would we use the + to concatenate strings in C++?

Take a look at Figure 12.5 for an example of this operation.

Figure 12.5. A little test program for an early version of the xstring class (codexstrtstc.cpp)
#include <iostream>
#include <string>
#include "xstringc.h"
using namespace std;
int main()
{
  xstring x = "Steve ";
  xstring y = "Heller";
  xstring z = x + y;
  cout << z << endl;
}

What we want this program to do is display my name on the screen by concatenating my last name on to the end of my first name (with a space in between so they don't run together). Unfortunately, it won't work, because the version of the xstring header file that it includes doesn't have the constructor that we've been discussing. That header file is shown in Figure 12.6.

Figure 12.6. An early version of the xstring header file (codexstringc.h)
#ifndef XSTRING_H
#define XSTRING_H

#include <iostream>
#include <string>

class xstring : public std::string
{
public:
  xstring();
  xstring(const xstring& Str);
  xstring(char* p);

  short find_nocase(const xstring& Str);
  bool less_nocase (const xstring& Str);
};
#endif

Okay, but exactly why doesn't this work? The problem is that the string concatenation operator, operator +, is part of the standard library string class, and its return type is std::string, not xstring. Therefore, the compiler gives an error message that looks like Figure 12.7:

Figure 12.7. An error message from mixing strings and xstrings
xstrtstc.cpp:
Error E2034 xstrtstc.cpp 11: Cannot convert 'string' to 'xstring' in function main()
*** 1 errors in Compile ***

How do we fix this? By declaring and implementing the constructor that automatically creates an xstring from a standard library string, xstring(const std::string& Str);, as shown in Figure 12.4 on page 863. Figure 12.8 shows the header file with that addition.

Figure 12.8. Another version of the xstring header file (codexstringd.h)
#ifndef XSTRING_H
#define XSTRING_H

#include <iostream>
#include <string>

class xstring : public std::string
{
public:
  xstring();
  xstring(const xstring& Str);
  xstring(const std::string& Str);
  xstring(char* p);
  xstring(unsigned Length, char Ch);
  xstring(unsigned Length);

  short find_nocase(const xstring& Str);
  bool less_nocase (const xstring& Str);
};

#endif

Once we add that constructor, the program in Figure 12.9 will compile successfully. Running it will display my name, as desired.

Figure 12.9. A successful attempt to mix strings and xstrings (codexstrtstd.cpp)
#include <iostream>
#include <string>
#include "xstringd.h"
using namespace std;

int main()
{
  xstring x = "Steve ";
  xstring y = "Heller";
  xstring z = x + y;
  cout << z << endl;
}

Now let's see why we need the other constructors in the xstring class. The next constructor, xstring(char* p);, creates an xstring from a C string literal. We need this to be able to initialize an xstring conveniently, such as in the statement xstring x = "Steve ";. The implementation of this constructor is trivial; it just initializes its base class string part with the C string literal value that we give it. Figure 12.10 shows the code for this constructor.

Figure 12.10. The char* constructor for the xstring class (from codexstring.h)
xstring::xstring(char* p)
: string(p)
{
}

Now how about the next constructor, xstring(unsigned Length, char Ch);? This constructor creates an xstring with a specified length, with all the characters set to a specified character. We need this one to help us with our formatting of our screen displays, as you'll see later in this chapter. Figure 12.11 shows the code for this constructor, which is also quite simple; it merely calls the corresponding constructor from the standard library string class.

Figure 12.11. Another constructor for the xstring class (from codexstring.h)
xstring::xstring(unsigned Length, char Ch)
: string(Length, Ch)
{
}

The last constructor for this type, while no more complex in its implementation, is considerably more interesting in the service that it provides for us.

Figure 12.12. The final constructor for the xstring class (from codexstring.h)
xstring::xstring(unsigned Length)
: string(Length, ' ')
{
}

As you can see, this is very similar to the previous constructor. The only difference is that you don't have to specify what character you want to fill in the string; it will automatically be filled in with blanks.

But this constructor also serves another very useful purpose, besides letting us type a few less characters when we want to create an xstring filled with a specified number of blanks. It prevents a certain potentially serious error that can happen when we initialize standard library strings.

Preventing Accidental Initialization by 0

Remember the problem I alerted you to under the heading “One of the Oddities of the Value 0” on page 777 in Chapter 11? I accidentally initialized a string member variable with the value 0 in one of the constructors for the HomeItem class. Since the value 0 is acceptable as a pointer of any type, this resulted in calling the string class constructor that takes a char* argument, passing the value 0 as the address from which to copy characters into the newly created string. The result was an incorrectly initialized string that would cause the program to blow up if it were ever used, because 0, of course, is not a valid memory address.

It would be very nice to prevent this problem by making the compiler reject the value 0 as an initializer for our xstring class. And that's what that most recently defined constructor does, as you'll see if you try to compile the program shown in Figure 12.13.

Figure 12.13. An illegal program (codestrzero.cpp)
#include <string>
#include "xstringd.h"
using namespace std;

main()
{
 xstring y(0);
 y = "abc";
}

Figure 12.14 shows the error message that you'll get if you try to compile this program:

Figure 12.14. The error message from compiling that illegal program (codestrzero.err)
strzero.cpp:
Error E2015 strzero.cpp 8: Ambiguity between 'xstring::xstring(char *)' and 'xstring:
:xstring(unsigned int)' in function main()
*** 1 errors in Compile ***

This is an excellent result, but exactly why are we getting an error in the first place?

Because we are turning one of the most irritating characteristics of the value 0 to our advantage. You see, while 0 is an acceptable value for a char*, it is also an acceptable value for an unsigned int. So because we have defined two different constructors that could take a 0 argument, the compiler complains that it can't tell which one we mean, and refuses to compile the offending statement. This lets us know about the improper initialization of one of our variables.

However, we're not quite out of the woods on this issue yet. Let's try another case where we really don't want an automatic conversion from a number to an xstring. Figure 12.15 shows an example of this sort of dubious conversion.

Figure 12.15. A legal but dubious program (codestrone.cpp)
#include <string>
#include "xstringd.h"
using namespace std;

main()
{
 xstring z(1);

 xstring y;
 y = 1;
}

With the latest version of the xstring header file we've seen so far, xstringd.h, this program will compile successfully. The first statement inside the function, xstring z(1);, is fine; it initializes an xstring to hold exactly one blank, as we intended. Of course, the second statement, xstring y; just declares another xstring variable, so that's okay as well. However, what about the third statement, y = 1;? What does that mean and why does it compile successfully?

That turns out to mean that we want to set the value of y to consist of one blank. That's because, as we have seen already in the discussion of our own string class, a constructor that takes exactly one argument will automatically be used to create a new object whenever necessary. In this case, we have a constructor for the xstring class that accepts unsigned integers. Since 1 can be considered an unsigned integer, and we are trying to assign an xstring the value 1, that constructor is called automatically.

This isn't quite as serious a problem as the one with 0, because at least the new xstring has a legitimate value, namely one blank character, rather than an invalid value that will cause the program to blow up. However, it is still not very desirable, because we can now set the value of an xstring to a series of blanks accidentally, just by assigning an integer value to it, when it is very unlikely that we mean to do that.

Luckily, there is a solution to this problem also. Let's see how it works.

The explicit Keyword

This is a job for the explicit keyword, which was added to C++ to allow class designers to solve a problem with constructors that resulted from a (usually convenient) feature of the language called implicit conversion. Under the rules of implicit conversion, a constructor that takes one argument is also a conversion function that is called automatically (or implicitly) where an argument of a certain type is needed and an argument of another type is supplied.[4]

[4] By the way, a constructor that takes more than one argument but has default arguments for all arguments after the first one is also a conversion function, because it can be called with one argument.

In many cases, an implicit constructor call is very useful; for example, it's extremely handy to be able to supply a char* argument when an xstring (or a const xstring&) is specified as the actual argument type in a function declaration. The compiler allows this without complaint because we have an xstring constructor that takes a char* argument and can be called implicitly. However, sometimes we don't want the compiler to supply this automatic conversion because the results would be surprising to the user of the class. In some previous versions of C++, it wasn't possible to prevent the compiler from supplying the automatic conversion, but in standard C++ we can use the explicit keyword to tell the compiler, in essence, that we don't want it to use a particular constructor unless we explicitly ask for it.

Figure 12.16 shows the final version of the xstring header file, with the explicit specification for the constructor that takes an integer type.

Figure 12.16. The final version of the xstring header file (codexstring.h)
#ifndef XSTRING_H
#define XSTRING_H

#include <iostream>
#include <string>

class xstring : public std::string
{
public:
  xstring();
  xstring(const xstring& Str);
  xstring(const std::string& Str);
  xstring(char* p);
  xstring(unsigned Length, char Ch);
explicit xstring(unsigned Length);

  short find_nocase(const xstring& Str);
  bool less_nocase (const xstring& Str);
};
#endif

To see how this prevents the error of trying to set an xstring to an integer value, let's analyze why trying to compile the program shown in Figure 12.17 produces an error. Note that this is the same program as the previous one shown in Figure 12.15 on page 870, except that it uses the final version of our xstring header file.

Figure 12.17. An illegal program (codestrfix.cpp)
#include <string>
#include "xstring.h"
using namespace std;

main()
{
 xstring z(1);

 xstring y;
 y = 1;
}

Figure 12.18 shows the error message that will be generated if you try to compile the above program.

Figure 12.18. The error message from strfix.cpp (codestrfix.err)
strfix.cpp:
Error E2285 strfix.cpp 11: Could not find a match for 'xstring::operator =(int)' in
 function main()
*** 1 errors in Compile ***

The reason that the line xstring z(1); is legal, given the definitions in xstring.h, is that we are explicitly stating that we want to construct an xstring by calling a constructor that will accept an integer value; in this case, that constructor happens to be xstring(unsigned).

By contrast, the line y = 1; will be rejected by the compiler. For this line to be legal, the xstring class defined in xstring.h would need a constructor that could be called implicitly to create an xstring from the literal value 1. Although that interface file does indeed define an xstring constructor that can be called with one argument of an integer type, we've added the explicit keyword to its declaration to tell the compiler that this constructor doesn't accept implicit calls.

This is how we prevent a user from accidentally calling the xstring(unsigned) constructor by providing an integer argument to a function that expects an xstring. Because the user is very unlikely to want an integer value such as 1 silently converted to an xstring of one blank, as the xstring(unsigned) constructor would do in this case, making this constructor explicit will reduce unpleasant surprises.[5]

[5] Note that the type of the argument to the constructor doesn't have to match the declaration of the constructor exactly to be acceptable. The value 1, for example, will match any unsigned or signed integer type. The argument matching rules are complicated, but luckily we don't have to worry about them further at this point.

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

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