9.6. The protected Access Specifier
Member variables and member functions that are listed in a protected section of the interface definition are treated as though they were private, with one important exception: member functions of derived classes can access them when they occur as the base class part of a derived class object.
In the current case, we've seen that a DatedStockItem is "just like" a StockItem with one additional member variable and some other additions and changes that aren't relevant here. The important point is that every DatedStockItem contains everything that a StockItem contains. For example, every DatedStockItem has a m_MinimumStock member variable because the StockItem class has a m_MinimumStock member variable, and we're defining DatedStockItem as being derived from StockItem. Logically, therefore, we should be able to access the value of the m_MinimumStock member variable in our DatedStockItem. However, if that member variable is declared as private, we can't, because the private access specifier doesn't care about inheritance. Since DatedStockItem is a different class from StockItem, any private member variables and functions that StockItem might contain wouldn't be accessible to member functions of DatedStockItem, even though the member variables of StockItem are actually present in every DatedStockItem! That's why we have to make those variables protected rather than private.
- Susan: I don't understand why, if DatedStockItem has those member variables, it wouldn't be able to access them if they were specified as private in the base class.
- Steve: Because the compiler wouldn't let DatedStockItem member functions access them; private variables are private to the class where they are defined (and its friends, if any) even if they are part of the base class part of a derived class object. That's why protected was invented.
- Susan: Well, friend lets a class or a function access something in another class, so what's the main difference between protected and friend?
- Steve: They're used in different situations. You can use friend when you know exactly what other class or function you want to allow to access your private or protected members. On the other hand, if all you know is that a derived class will need to access these members, you make them protected.
In other words, a protected variable or function is automatically available to any derived class, as it applies to the base class part of the derived class object. To make a class or function a friend to a class being defined, you have to name the friend class or friend function explicitly.- Susan: What do you mean by "base class part of the derived class object"? I'm fuzzy here.
- Steve: Every DatedStockItem (derived class) object contains a StockItem (base class) object and can use all of the non-private member functions of StockItem, because DatedStockItem is derived from StockItem. This is what allows us to avoid having to rewrite all the code from the base class in the derived class.
- Susan: Yes, but how is that different from just a regular base class object?
- Steve: Well, one difference is that a base class part of a derived class object only exists inside a derived class object, not by itself, and must be initialized as part of the derived class object's initialization. A less obvious difference, though, is that protected variables and functions are accessible to derived class objects only when they are part of the base class part of a derived class object of that type, not when they are part of a freestanding base class object. For example, if one of the arguments to DatedStockItem function was a StockItem object, that DatedStockItem function could not access any of the protected members of that StockItem object.
- Susan: I don't get this. I need some pictures to clear up these base class and derived class things.
- Steve: Okay, I'll give it a shot. Take a look at Figure 9.10, where I've used a dashed-dotted line around the base class, StockItem, to indicate its boundaries as a base class part. I've also used different line types in the boxes around the member variables and functions to indicate the access specifiers of those member variables and functions: a solid box to indicate private members, a dashed box to indicate protected members, and a dotted box to indicate public members. Finally, a solid arrow pointing to a variable indicates the ability to access the variable at the point of the arrow, while the dotted line going from the derived class object to m_UPC indicates that it is inaccessible to the derived class object.
As I told Susan, Figure 9.10 illustrates a hypothetical DatedStockItem class. Here, m_Name, m_Price, and m_InStock are protected base class member variables, whereas m_UPC is a private member variable and GetPrice() is a public member function.1
According to this scenario, the derived class member functions can access m_Name, m_Price, and m_InStock. Of course, any member function can access any public member of any other class, so GetPrice is accessible to DatedStockItem member functions as well. However, with this setup member functions of DatedStockItem could not access m_UPC, even though this member variable would actually be present in the base class part of a DatedStockItem.
Now that we've cleared up that point (I hope), we have to consider the question of when to use protected versus private variables. The private member variables of the base class cannot be accessed directly by derived class member functions. This means that when we define the base class, we have to decide whether we want to allow any derived classes direct access to some of the member variables of the base class part of the derived object. If we do, we have to use the protected access specifier for those member variables. If we make them private and later discover that we need access to those variables in a derived class, we then have to change the definition of the base class so that the variables are protected rather than private. Such changes are not too much trouble when we have written all of the classes involved, but they can be extremely difficult or even impossible when we try to derive new classes from previously existing classes written by someone else.
However, protected members (especially protected variables) have some of the drawbacks of public variables and functions. Anyone can define a new derived class that uses those variables or functions, and any changes to those base class variables or functions will cause the code in the derived class to break. Hence, making everything protected isn't a panacea.
Analogously to our earlier discussion of public member variables versus public member functions, the drawbacks of protected variables are more serious than those of protected member functions, which at least don't commit you to specific implementation details. In the present case, I doubt that there would be any significant difference in maintainability between these two approaches, as we are designing both the base and derived classes. But in a larger project it might very well be better to use protected member functions to allow access to private member variables in the base class rather than to use protected member variables for the same purpose.
The moral of the story is that it's easier to design classes for our own use and derivation than for the use of others. Even though we can go back and change our class definitions to make them more flexible, that alternative may not be available to others. The result may be that our classes will not meet others' needs.
- Susan: I don't get your moral of the story. Sorry.
- Steve: The moral is that when designing classes that may be used by others as base classes, we have to know whether those others will ever need access to our member variables. If we are in charge of all of the classes, we can change the access specifiers easily enough, but that's not a very good solution if someone else is deriving new classes from our classes.
- Susan: Okay, I guess. But what does that have to do with using protected variables or private ones with protected member functions?
- Steve: Only that if we used private variables with protected member functions to access them, we could allow the derived class to use the member variables in our base class in a controlled way rather than an uncontrolled one, and therefore could keep some say in how they are used. Unfortunately, this solution still requires us to figure out how the derived class member functions may want to use our member variables, so it isn't a "silver bullet".
- Susan: I still don't understand why we need to worry about who else is going to use our classes; who are these other people?
- Steve: One of the main advantages claimed for object-oriented programming is that it allows "division of labor"; that is, some programmers can specialize in building classes while others can specialize in writing application programs. This can increase productivity greatly, just as it does in medicine (e.g., general practitioners, specialists, nurses, and lab technicians).
- Susan: Okay, but what does that have to do with access specifiers? Why don't we just make everything public and avoid all this stuff?
- Steve: If we are going to let others use our classes, we should design them to be easy to use correctly and hard to use incorrectly. That's one of the main reasons we use private and protected: so we can determine where in our program an error might be caused. If we notice that one of our private member variables is being changed when it shouldn't be, we know where to look: in the code that implements the class. Because the member variable is private, we don't have to worry that it's being changed somewhere else. This is not the case with a public member variable, which can be modified anywhere in the program. If you'd ever had to try to find out where a variable is being modified in a gigantic program in C or another language that doesn't have private variables, you would know exactly what I mean!
After that excursion into the use of the protected access specifier and its impact on class design, let's look at the revised test program in Figure 9.11.
The new test program, Itmtst21, is exactly the same as its predecessor, itmtst20.cpp, except that it #includes the new header files item21.h and invent21.h (shown in Figures 9.12 and 9.13, respectively) and uses different input and output file names.2
Assuming that you've installed the software from the CD in the back of this book, you can try out this program. First, you have to compile it by following the compilation instructions on the CD. Then type itmtst21 to run the program. However, this program doesn't have much of a user interface, so you might want to watch it operating under the debugger by following the usual instructions for that method. When the program terminates, you can look at its output file, shop21.reo, to see what the reorder report looks like; if you do, you will see that it includes instructions to return some expired items.
Now that we've seen the results of using the new versions of our inventory control classes, let's take a look at the interface definitions of StockItem and DatedStockItem (Figure 9.12) as well as the implementation of those classes (Figure 9.13). I strongly recommend that you print out the files that contain these interfaces and their implementation for reference as you go through this section of the chapter; those files are code\item21.h and code\item21.cpp, respectively.3
- Susan: So you just write your new class right there? I mean you don't start over with a new page or something; shouldn't it be a different file or coded off all by itself somewhere? How come it is where it is?
- Steve: We could put it in another file, but in this case the classes are being designed together and are intended to be used interchangeably in the application program, so it's not unreasonable to have them in the same file. In other circumstances, it's more common to have the derived class in a separate file. Of course, sometimes you don't have any choice, such as when you're deriving a new class from a class that you didn't create in the first place and may not even have the source code for; in that case, you have to create a separate file for the derived class.