Friday, November 27, 2009

Scope Rules and Name Resolution Under Inheritance




I l@ve RuBoard


Scope Rules and Name Resolution Under Inheritance


In C++, class scope can be viewed as nested under derivation. From this point of view, the scope of a derived class is enclosed by the scope of its base class.



According to the general theory of nested scopes, whatever is defined in the inner scope is invisible in the outer, more global scope. Conversely, whatever is defined in the outer scope is visible in the inner, more local scope. In the next example, variable x is defined in the outer function scope, and variable y is defined in the inner block scope. It is appropriate to access the variable x in the inner scope. It is futile to access the variable y from the outer scope.





void foo()
{ int x; // outer scope: equivalent to base class
{ int y; // inner scope: equivalent to derived class
x = 0; } // ok to access the name from outer scope
y = 0; } // syntax error: inner scope is invisible outside



In this example, the outer scope plays the role of the base class and its members. The inner scope plays the role of the derived class and its members. From the derived class, you can access the members of the base class, but the members of the derived class cannot be accessed from the base class.



This means that the derived class members are invisible in the scope of the base class. This should agree with your intuition because the base class should be designed, implemented, and compiled before the derived class is written. So it is only natural that the base class member functions cannot access the derived class data members or member functions.



Conversely, base class members are in the outer scope and hence are visible in the derived class methods. Again, this concurs with your intuition because the derived class object "is a" base class object and has all member functions and data members that the base class has. From this point of view, the scope model of the relationship between the base and derived classes is not particularly helpful because it does not add much to your intuition. However, this model is very useful if the derived and base classes use the same names. Different languages use different rules to resolve these name conflicts, and the nested scope model, which is employed by C++, might be helpful in developing your intuition for writing C++ code.



The derived class scope is nested within the base class scope, which means that the derived class names hide base class names within the derived class. Similarly, the derived class names hide base class names in the derived class client code. This is a very important rule that should become part of your programming intuition: If the derived and base classes use the same name, the base class name does not have a chance, as the meaning of the derived class name will be used.



Let us clarify this rule. If a name without a scope operator is found in a derived class member function, the compiler tries to resolve the name as a name local to that member function. In the next code example, there are four variables that use the name x. All these variables are of the same type, but this is not important. They could be of different types, or some of these names could denote a function; the general rule I am discussing will stand anyway.





int x; // outer scope: can be hidden by class or function
class Base {
protected: int x; // base name hides global names
} ;

class Derived : public Base {
int x; // derived name hides base names
public:
void foo()
{ int x;
x = 0; } } ; // local variable hides all other names

class Client {
public:
Client()
{ Derived d;
d.foo(); } } ; // using object d as a target message

int main()
{ Client c; // define the object, run the program
return 0; }



In this code, you see a local variable in the member function foo() in the class Derived, a data member in the class Derived, a data member in the class Base, and a global variable in the file scope. The statement x = 0; in Derived::foo() sets the local variable x to zero. The derived data member Derived::x, the base data member Base::x, and the global name x are all hidden by this local name because the local name is defined in the most nested scope.



Comment out the definition of the variable x in the method foo(). The statement x = 0; now cannot be resolved to the local variable because this name will not be found. If the name is not found in the scope of the statement (in this case, the derived class member function), the compiler looks up the derived class scope among class data members or member functions, depending on the syntax of the reference to the name. In the above code example, if the local variable x in Derived::foo() were absent, it would be the derived data member Derived::x that would be set to zero by the statement x = 0; in the derived member function Derived::foo().



If the name mentioned in the member function is not found in the class scope either, the compiler searches the base class (and ancestor classes of the base class if they exist and if the name is not found in the base). The first name found in this search would be used to generate object code. In the code example, if both variables x in the Derived class were absent (the local variable and the data member), it would be the data member Base::x that would be set to zero by the statement x = 0;.



Finally, if the name is not found in any of the base classes, the compiler searches for the name declared in the file scope (as a global object defined in the file scope or an extern global object declared in this scope but defined elsewhere). If the name is found in this process, it is used; if not, it is a syntax error. In the code example, if neither class Derived nor class Base used the name x, the global variable x would be set to zero by the statement in Derived::foo().



Similarly, if a client of a derived class sends a message to a derived class object, the compiler searches the derived class first, and only after that does it look up the base class definition (or the base ancestor definition). If the derived class and one of its base classes use the same name, the derived class interpretation is used. The base names are not even looked up by the compiler if the name is found in the derived class. The derived class name hides the base class name, and the base name does not have a chance.



A modified example with two classes, Base and Derived, is shown next. There are two functions foo() in this example: One is a public member function of class Base, and the other is a public member function of class Derived. Similar to the previous example, the client code defines an object of the Derived class and sends the foo() message to that object. Since the Derived class defines the member function foo(), the derived member function is called. If the Derived class did not define function foo(), then the compiler would generate a call to the Base class function foo(). The Base class function has a chance only if the same name is not used by the Derived class.





class Base {
protected: int x;
public:
void foo() // Base name is hidden by the Derived name
{ x = 0; } } ;

class Derived : public Base {
public:
void foo() // Derived name hides the Base name
{ x = 0; } } ;

class Client {
public:
Client()
{ Derived d;
d.foo(); } } ; // call to the Derived member function

int main()
{ Client c; // create an object, call its constructor
return 0; }



Notice that in this example I do not introduce the global scope. If neither Derived nor Base class (nor any of its ancestors) has a member function foo(), then the function call to d.foo() is a syntax error. If a function foo() were defined in the global scope, the function call d.foo() would not call this global function anyway.





void foo()
{ int x = 0; }



This global function is not hidden by the foo() member function in the Derived (or the Base) class because it has a different interface. The member functions are called with the use of a target object, and the global function is called using the function name only:





foo(); // call to a global function



The function calls we are discussing have a different syntactic form:





d.foo(); // call to a member function



This syntactic form cannot be satisfied by a call to a global function梚t includes a target object and hence can be satisfied only by a class member function.




Name Overloading and Name Hiding


Notice that in the previous discussion, the function signature was not mentioned as a factor to consider. This is not an omission. The function signature is not a factor. The signature does not matter.



Of course I am being facetious. The function signature does matter when the compiler decides whether the actual argument matches the function's formal parameters. However, it does not matter for the resolution of nested inheritance scopes. If the name is found in the derived class, the compiler stops its search of the inheritance chain. What happens if the function found in the derived class is no good from the point of view of argument matching? Too bad梱ou have a syntax error. What if the base class has a better match, a function with the same name, and with the signature that matches the function call exactly? Too bad; it is too late: The base function does not stand a chance.



Unfortunately, this is quite counterintuitive for many programmers. Please try to work with these nesting rules and on the examples to make sure you hone your intuition accordingly. Next is an example from my experience. I have pruned everything not related to the issue of hiding in nested scopes and left only a small part of the code.




Listing 13.14 shows the simplified part of the hierarchy of accounting classes. I use class Account and class CheckingAccount only. The derived class overwrites the base member function withdraw(), but this is not going to play any role in the discussion. The client code defines CheckingAccount objects, sends them messages that belong to either the base class (getBal() and deposit()) or the derived class itself (withdraw()), and everything is fine. The output of the program run is presented in Figure 13-10.





Figure 13-10. Output for the program code in Listing 13.14.










Example 13.14. Example of inheritance hierarchy for Account classes.


#include <iostream>
using namespace std;

class Account { // base class
protected:
double balance;

public:
Account(double initBalance = 0)
{ balance = initBalance; }

double getBal() // inherited without change
{ return balance; }

void withdraw(double amount) // overwritten in derived class
{ if (balance > amount)
balance -= amount; }

void deposit(double amount) // inherited without change
{ balance += amount; }
} ;

class CheckingAccount : public Account { // derived class
double fee;

public:
CheckingAccount(double initBalance)
{ balance = initBalance; fee = 0.2; }

void withdraw(double amount) // it hides base class method
{ if (balance > amount)
balance = balance - amount - fee; }
} ;

int main()
{
CheckingAccount a1(1000); // derived class object
a1.withdraw(100); // derived class method
a1.deposit(200); // base class method
cout << " Ending balances\n";
cout << " checking account object: " << a1.getBal() <<endl;
return 0;
}


Although the client code here is only a few lines long, in real life, it was about 200 pages long. The program evolved to reflect the changes in business conditions. One of the changes required was to add to class CheckingAccount yet another function deposit(), which could be used for international wire transfers. In these transfers, a transaction fee would be imposed depending on the amount and source of the transfer. This fee could be computed by the client code and sent to the CheckingAccount class as an argument. Hence, a simple way to support this change was to write another function deposit() with two parameters.





void CheckingAccount::deposit(double amount, double fee)
{ balance = balance + amount - fee; }



The client code for processing international transfers and for computing the fee required only a few pages to be added to the program. Here is an example of the new client code that calls this new deposit() function.





a1.deposit(200,5); // derived class method



So far, so good. The change went well, the new code ran fine. There was, however, a problem during system integration. These 200 pages of code that used to work so well before the change now did not work as well. Actually, the code did not work at all and would not even compile.



Now, let me assure you that I used many languages before using C++, and I had never seen anything like this. I also suspect that whatever languages you used before C++, you never saw anything like this either. This is yet another contribution of C++ to software engineering that you should be aware of.



Of course, we all have been in situations where adding some new code breaks the existing code, which no longer works correctly. Usually this happens because the new code interferes with the data that the existing code relies upon. But the existing code always compiles. In traditional languages, when you add new code, you do not get syntax errors in existing code.



In C++, a program consists of classes that are linked to each other, not only through data but also through inheritance. Of course, the new code can make the existing code semantically incorrect by handling data incorrectly. This is possible in any language. But the new code can also make the existing code syntactically incorrect through the inheritance links! This is only possible in C++. This is why I press this point about programming intuition, needing to know the rules and developing a feel for correct and incorrect C++ code.



Let us take a look at the reason for this "innovative" kind of programming trouble. This is how my new class CheckingAccount looks.





class CheckingAccount : public Account {
double fee;
public:
CheckingAccount(double initBalance)
{ balance = initBalance; fee = 0.2; }
void withdraw(double amount) // it hides base class method
{ if (balance > amount)
balance = balance - amount - fee; }
void deposit(double amount, double fee) // new method
{ balance = balance + amount - fee; } // hides base method
} ;



When the compiler was processing the existing 200 pages of client code, the calls to the member function deposit() were aiming at the base class member function Account::deposit() with one parameter.





a1.deposit(200); // base class method?



According to the rules for the name resolution you just saw, the compiler analyzes the type of the message target, finds that the object a1 belongs to class CheckingAccount, and searches the class CheckingAccount for a member function whose name is deposit(). The compiler finds this function and stops the search through the inheritance chain. The next phase is, of course, signature matching. The compiler discovers that the method CheckingAccount::deposit() found in the derived class has two parameters. Meanwhile, the client code (which wanted to call the base class method) supplies only one parameter. The compiler tells me in no uncertain terms that I have a syntax error.



I probably should have saved the joke about driving the tank for this discussion. It was clear to me that my code was correct and yet I found another bug in my compiler. (It does not matter what compiler it was. When learning a new language, you always find quite a few bugs in your compiler until you know the language better.)



I would have liked very much if my compiler had treated this situation as function name overloading. I had the existing deposit() function with one parameter in the base class. I had the new deposit() function with two parameters in the derived class. But the object of the derived class was also an object of the base class! It had the inherited deposit() function with one parameter as well. My intuition was that the derived class had two deposit() functions, one with one parameter and the other with two parameters. And I would have liked very much if the compiler had used the rules for function name overloading and had picked up the right function, the one with only one parameter. However, as I said before, when a base method is hidden by a derived class method, the base method does not stand a chance. The overloading applies to several functions in the same scope. Hiding takes place between functions in nested scopes. Finally, I gave up and changed my thinking. It takes time, but I am sure you will be able to do the same.



ALERT



C++ supports function name overloading in the same scope only. In independent scopes, function names do not conflict, and you can use the same name with the same or different signatures. In nested scopes, the name in the nested scope hides the name in the outer scope, whether or not these names have the same signatures. If classes are related through inheritance, the function name in the derived class hides the function name in the base class. Again, the signature is not important.






Figure 13-11 shows an object of the derived class with these two functions, one coming from the base class and the other coming from the derived class. The vertical arrow from the client code shows you that the compiler starts the search in the derived class. The compiler stops as soon as the name match is found (with any signature), and no attempt is made to get to the base class using the rules for name overloading. If the concepts of nested scopes for inheritance sound too abstract to you, use this picture to remind yourself that the search stops at the first match.





Figure 13-11. How a derived class method hides a base class method in a derived class object.










Calling a Base Method Hidden by the Derived Class


There are several remedies for this situation. One remedy is to indicate in the client code what function should be called. The scope operator does the job well.





int main()
{ CheckingAccount a1(1000); // derived class object
a1.withdraw(100); // derived class method
// a1.deposit(200); // syntax error
a1.Account::deposit(200); // solution to the problem
cout << " Ending balances\n";
cout << " checking account object: " << a1.getBal() <<endl;
return 0; }



Please make sure you do not get excited about this solution. The obvious drawback of this solution is that it requires making changes to the existing code. The advantage of the object-oriented approach is that it favors adding to the existing code rather than modifying it. This solution, however, is labor extensive and error prone. To use this solution is to ask for trouble.



From the software engineering point of view, this solution contradicts the principles of writing C++ code I discussed earlier. Which principles? Well, who bears the burden of the work in this solution? The client code. Who should carry the burden of the solution according to the principles of writing code? The server code. This solution fails to push responsibility down to the server classes. Instead, it brings responsibility up to the client code: you need to make sure that the base function is called梚ndicate explicitly that the base function should be called. This is a brute force solution.



Make sure that you use the criterion of pushing responsibility to the server classes in your work. It indicates in what direction you should search for a good solution. Let us look at the Account inheritance hierarchy. Our goal should be to add to these classes a method (or methods) that would make the problem go away. Why would I want to add a method? Because I do not want to change existing methods. Why would I want to add methods to the inheritance hierarchy? Because these classes serve the client code, and I want to push responsibility to the server classes.



One remedy is to overload the deposit() method in the base class rather than in the derived class. Since both functions belong to the same class and hence to the same scope, you have a case of legitimate C++ function name overloading. Both functions are inherited by the derived class and can be called through the derived class object as the message target. Here is the example of this solution.





class Account { // base class
protected:
double balance;
public:
Account(double initBalance = 0)
{ balance = initBalance; }
double getBal() // inherited without change
{ return balance; }
void withdraw(double amount) // overwritten in derived class
{ if (balance > amount)
balance -= amount; }
void deposit(double amount) // inherited without change
{ balance += amount; }
void deposit(double amount,double fee) // overloads deposit()
{ balance = balance + amount - fee; } } ;

class CheckingAccount : public Account { // derived class
double fee;
public:
CheckingAccount(double initBalance)
{ balance = initBalance; fee = 0.2; }
void withdraw(double amount) // hides the base class method
{ if (balance > amount)
balance = balance - amount - fee; } } ;

int main()
{ CheckingAccount a1(1000); // derived class object
a1.withdraw(100); // derived class method
a1.deposit(200); // existing client code
a1.deposit(200,5); // new client code
cout << " Ending balances\n";
cout << " checking account object: " << a1.getBal() << endl;
return 0; }



This is a good workaround. Notice that the solution is found in the form of adding code to the server class, not in the form of modifying the client code. This solution pushes the work to the Account class, and this is good. However, this solution requires opening and changing the base class and not the derived class. This is not desirable for configuration control reasons. The higher a class is in the inheritance hierarchy, the more we want to guard this class against change because the change can affect other derived classes. The lower a class is in the inheritance hierarchy, the safer it is to open and to change.



Another problem with this solution is that the scope rules allow a base class member function to access base class data members only, not the derived class data. In my example, this is not a problem; both deposit() methods need only the base class data. Often, however, this is not so. The new method might need data that is defined in the derived class and is not available in the base class. For example, the standard withdrawal fee might be imposed on the deposit transaction as well. Then the new method deposit() could be implemented in the derived class only.





void CheckingAccount::deposit(double amount, double fee)
{ balance = balance + amount - fee - CheckingAccount::fee; }



However, putting the new method deposit() into the derived class takes us back to square one with the problem of the nested name scopes梩his function hides the base class deposit() function and renders the existing code, with the calls to deposit() with one argument, syntactically incorrect.



A better remedy to this problem is to bite the bullet and place the new deposit() method where it belongs: in the derived class. To make the existing calls to the deposit() function with one legitimate parameter, you can overload the deposit() function in the derived class rather than in the base class. Again, the derived class is a server class for the client code, and this solution pushes responsibility to the server class.



TIP



Always look for ways to write C++ code such that the responsibility is pushed from the client code to the server code; thus the client code expresses the meaning of computations, not details of computations. This is a very general principle. It will serve you well.






Listing 13.15 shows this solution. The derived class has two member functions deposit() with two different signatures. Since they both belong to the same class, the rules for name overloading stand. Both new code and existing code now call the member functions of the derived class using different signatures. All that the member functions with one argument should do is call the base class member function with the same name (push the work to the server). The output of the program run is presented in Figure 13-12.





Figure 13-12. Output for program in Listing 13.15.










Example 13.15. Example of inheritance hierarchy for Account classes.


#include <iostream>
using namespace std;

class Account { // base class
protected:
double balance;
public:
Account(double initBalance = 0)
{ balance = initBalance; }
double getBal() // inherited without change
{ return balance; }
void withdraw(double amount) // overwritten in derived class
{ if (balance > amount)
balance -= amount; }
void deposit(double amount) // inherited without change
{ balance += amount; }
} ;

class CheckingAccount : public Account { // derived class
double fee;

public:
CheckingAccount(double initBalance)
{ balance = initBalance; fee = 0.2; }

void withdraw(double amount)
{ if (balance > amount)
balance = balance - amount - fee; }

void deposit(double amount) // hides the base class method
{ Account::deposit(amount); } // call to a base function

void deposit(double amount, double fee) // hides base method
{ balance = balance + amount - fee - CheckingAccount::fee; }
} ;

int main()
{
CheckingAccount a1(1000); // derived class object
a1.withdraw(100); // derived class method
a1.deposit(200); // existing client code
a1.deposit(200,5); // new client code
cout << " Ending balances\n";
cout << " checking account object: " << a1.getBal() <<endl;
return 0;
}


Make sure that you are not intimidated by the use of scope operators in this example. The function deposit() with one parameter in class CheckingAccount could have been written this way:





void CheckingAccount::deposit(double amount) // hides base method
{ deposit(amount); } // infinite recursive call



When the compiler processes the body of this function, it first looks for a match between the name deposit() and a name local to the function. There are no names local to the function, so the compiler looks for a match among class members. It finds the name CheckingAccount::deposit() and generates a call to it. As a result, the call is interpreted as an infinite recursive call.



The scope operator in Listing 13.15 directs the compiler to generate a call to the base function Account::deposit() and to avoid the trap of recursion. Notice that the responsibility to deal with the class hierarchy and decide to which class the deposit() function belongs is pushed down to the server class and not up to the client code as in my first remedy.



The function deposit() with two parameters in class CheckingAccount could have been written this way.





void CheckingAccount::deposit(double amount, double fee)
{ balance = balance + amount - fee - fee; }



When the compiler processes the body of this function, it looks for a match between the name fee and a name local to the function. This name is the name of the function's second parameter. Even though the class CheckingAccount has a data member fee, this data member is hidden by the name of the function parameter. To access the class data member fee, the code in Listing 13.15 has to use the scope operator that overrides the scope rules.





Using Inheritance for Program Evolution


Often a good way to handle this kind of program evolution is to avoid the problem and its remedies altogether. The source of my difficulties with international wire transfers in Listings 13.14 and 13.15 was that I was trying to change existing code (classes Account and CheckingAccount) to accommodate new conditions.



This is a natural way of thinking梖rom the traditional programming point of view. Object-oriented programming supported by C++ offers you an opportunity to think differently. Instead of looking for ways to change existing code, you could look for the ways to inherit from existing classes to support new requirements.



Make no mistake桰 am talking about a new way of thinking about writing code. Using inheritance means that you are writing new code instead of changing existing code. Everyone who has ever tried to change existing code knows there is a world of difference between these two approaches. C++ offers a new approach to this little international wire transfer problem: leave the existing 200 pages alone, leave classes Account and CheckingAccount frozen, and introduce yet another derived class to support the new client code:





class InternationalAccount : public CheckingAccount { // great!
public:
InternationalAccount(double initBalance)
{ balance = initBalance; }
void deposit(double amount, double fee) // hides base method
{ balance = balance + amount - fee - CheckingAccount::fee; }
} ;




Listing 13.16 shows this solution. The classes Account and CheckingAccount are the same as in Listing 13.14. Yet another derived class, InternationalAccount, introduces no additional data members and only one member function, the function deposit(), which satisfies new client requirements. Since the objects, which are the targets of the deposit() messages with different numbers of parameters, belong to different classes, the issue of hiding or overloading does not arise. The object a1 is a target of the message with one parameter, and the compiler calls the function of the base class. The object a2 is a target of the message with two parameters, and the compiler calls the function of the class InternationalAccount derived from the class CheckingAccount. The output of the program run is presented in Figure 13-13.





Figure 13-13. Output for the program in Listing 13.16.










Example 13.16. Example of enhanced inheritance hierarchy for Account classes.


#include <iostream>
using namespace std;

class Account { // base class
protected:
double balance;
public:
Account(double initBalance = 0)
{ balance = initBalance; }
double getBal() // inherited without change
{ return balance; }
void withdraw(double amount) // overwritten in derived class
{ if (balance > amount)
balance -= amount; }
void deposit(double amount) // inherited without change
{ balance += amount; }
} ; // no changes to existing class

class CheckingAccount : public Account { // derived class
protected:
double fee;

public:
CheckingAccount(double initBalance = 0)
{ balance = initBalance; fee = 0.2; }

void withdraw(double amount) // hides the base class method
{ if (balance > amount)
balance = balance - amount - fee; }
} ; // no changes to existing class

class InternationalAccount : public CheckingAccount { // great!
public:
InternationalAccount(double initBalance)
{ balance = initBalance; }

void deposit(double amount, double fee) // hides base method
{ balance = balance + amount - fee - CheckingAccount::fee; }
} ; // work is pushed to a new class

int main()
{
CheckingAccount a1(1000); // derived class object
a1.withdraw(100); // derived class method
a1.deposit(200); // base class method
InternationalAccount a2(1000); // new server object
a2.deposit(200,5); // derived class method
cout << " Ending balances\n";
cout << " First checking account object: "
<< a1.getBal() << endl;
cout << " Second checking account object: "
<< a2.getBal() << endl;
return 0;
}


This is a very useful technique of program evolution. Instead of butchering existing classes and dealing with the dangers of invalidating existing client code, you derive another class from existing classes, which is responsible only for new program functionality. The use of C++ inheritance is the cornerstone of this new approach to software maintenance: writing new code instead of modifying existing code.



Actually, class CheckingAccount does need some modifications. The first modification is making the private data member fee protected to make sure that the new derived class, InternationalAccount, is able to access this data member. Another approach would be to add to class CheckingAccount a member function that retrieves the value of this data member; the client code (in this case, InternationalAccount) would call this function to access the base class data. As I mentioned earlier, I prefer to make a few data members accessible to one or two derived classes than to create a set of access functions that will be used only by these new derived classes (in this example, just one derived class).



Another way to avoid this modification to the existing class CheckingAccount is to exercise more foresight at the time of the class design. Why do you make class data members private? According to the principles of object-oriented programming, you do it for several reasons:





  • You do not want the client code to create dependencies on server class data names.





  • You do not want to complicate the client code with direct operations over data.





  • You do not want the client code to know more about server design than is necessary.





  • You want the client code to call server methods whose names explain the actions.





  • You want the client code to push responsibility for lower level details to servers.





Notice that all these goals can be achieved by making server class data members protected rather than private. As I mentioned earlier, the protected keyword works like other access right modifiers, private and public, relative to different categories of class users. For derived classes, which are linked to the class by inheritance, the keyword protected works exactly as public works. It allows the derived classes direct access to the base class members. For client classes, which are not linked to the class by inheritance, the keyword protected works exactly as private does. There is no difference. If you think that program evolution through inheritance is possible, use protected access rights rather than private.



TIP



Always look for ways to use C++ inheritance for program evolution. Push responsibility from the client code to new derived classes. Weigh this approach against the drawbacks of creating too many small classes.





I am careful to say that the issue here is program evolution rather than initial program design. During program design, some key base classes might wind up at the top of a tall inheritance hierarchy of classes that includes many derived classes. With a large number of potential class users, the issues of data encapsulation, information hiding, pushing responsibilities to servers become important. For these key classes, you might want to use the private modifiers to force even derived classes to use access functions. For program evolution, the classes you will be using for further derivations are themselves at the bottom of the inheritance hierarchies (class CheckingAccount is a good example). They will not have a large number of derived classes dependent on them, and the issues of data encapsulation, information hiding, and pushing responsibilities to servers lose their importance with the decrease in the number of dependent classes.



The second modification is in the class CheckingAccount constructor. I added the default parameter value to avoid a syntax error in the client code when the object of the class CheckingAccount was created. This is similar to the issues I discussed for composite classes in Chapter 12. In the next section, I will discuss these issues as applied to the creation of C++ derived objects.








I l@ve RuBoard

No comments: