Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Books
  Краткое описание
 Linux
 W. R. Стивенс TCP 
 W. R. Стивенс IPC 
 A.Rubini-J.Corbet 
 K. Bauer 
 Gary V. Vaughan 
 Д Вилер 
 В. Сталлинг 
 Pramode C.E. 
 Steve Pate 
 William Gropp 
 K.A.Robbins 
 С Бекман 
 Р Стивенс 
 Ethereal 
 Cluster 
 Languages
 C
 Perl
 M.Pilgrim 
 А.Фролов 
 Mendel Cooper 
 М Перри 
 Kernel
 C.S. Rodriguez 
 Robert Love 
 Daniel Bovet 
 Д Джеф 
 Максвелл 
 G. Kroah-Hartman 
 B. Hansen 
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 MINIX...3057 
 Solaris...2933 
 LD...2905 
 Linux Kernel 2.6...2470 
 William Gropp...2182 
 Rodriguez 6...2015 
 C++ Templates 3...1945 
 Trees...1938 
 Kamran Husain...1866 
 Secure Programming for Li...1792 
 Максвелл 5...1710 
 DevFS...1694 
 Part 3...1684 
 Stein-MacEachern-> Час...1632 
 Go Web ...1627 
 Ethreal 4...1619 
 Стивенс 9...1607 
 Arrays...1607 
 Максвелл 1...1593 
 FAQ...1539 
 
  01.01.2024 : 3621733 посещений 

iakovlev.org

Здесь мы рассмотрим статическую и динамическую проверку типов. Статическая проверка-это всегда хорошо,в то время как динамической нужно пользоваться осторожно. В этом случае полезно использовать runtime type identification (RTTI).

    Что такое статическая проверка типов?

    Static type checking, sometimes known as static typing, is when the compiler checks the type correctness of operations at compile time. For example, the compiler checks the parameter types of function arguments and checks that a member function invocation is guaranteed to work at runtime, then it flags improper matches as errors at compile time.

    In object-oriented programs, the most common symptom of a type mismatch is the attempt to invoke a member function via a reference to an object, where the reference's type and/or the object's type does not support the member function. For example, if class X has member function f() but not member function g() and x is an instance of class X, then x.f() is legal and x.g() is illegal.

     class X {
     public:
       void f() throw();
     };
     
     void X::f() throw()
     { }
     
     int main()
     {
       X x;
       x.f();   //OK
     
       #ifdef GENERATE_ERROR
         //The following error is caught at compile time
         //There is no need for runtime checks
         x.g();
       #endif
     }
     

    Fortunately, C++ catches errors like this at compile time.

      Что такое динамическая проверка типов?

      Dynamic type checking, sometimes known as dynamic typing, is the determination of type correctness at runtime.

      With dynamic type checking, user code determines whether an object supports a particular member function at runtime rather than at compile time. Dynamic type checking is often accompanied by downcasts (see FAQ 27.11) and can unnecessarily increase the cost of C++ software. Runtime type identification (RTTI) is one kind of dynamic type checking that is supported directly by C++.

      The following example demonstrates the wrong way to do things. Pretend that the various escape sequences toggle italics on the various kinds of printers.

       #include <iostream>
       using namespace std;
       
       // pretend this is the escape character
       const char* const esc = "ESC";
       
       enum Type { EPSON, PROPRINTER, STAR };
       class Printer {
       public:
         virtual ~Printer() throw();
         virtual Type type() const throw() = 0;
       };
       
       Printer::~Printer() throw()
       { }
       
       class EpsonPrinter : public Printer {
       public:
         virtual Type type() const throw();
         void italicsEpson(const char* s) throw();
       };
       
       Type EpsonPrinter::type() const throw()
       { return EPSON; }
       
       void EpsonPrinter::italicsEpson(const char* s) throw()
       { cout << esc << "i+" << s << esc << "i-"; }
       
       class ProprinterPrinter : public Printer {
       public:
         virtual Type type() const throw();
         void italicsProprinter(const char* s) throw();
       };
       
       Type ProprinterPrinter::type() const throw()
       { return PROPRINTER; }
       
       void ProprinterPrinter::italicsProprinter(const char* s) throw()
       { cout << esc << "[i" << s << esc << "[n"; }
       
       class StarPrinter : public Printer {
       public:
         virtual Type type() const throw();
         void italicsStar(const char* s) throw();
       };
       
       Type StarPrinter::type() const throw()
       { return STAR; }
       
       void StarPrinter::italicsStar(const char* s) throw()
       { cout << esc << "x" << s << esc << "y"; }
       
       void printUsingItalics(Printer& p, const char* s) throw()
       {
         switch (p.type()) {
           case EPSON:
             ((EpsonPrinter&) p).italicsEpson(s);
             break;
           case PROPRINTER:
             ((ProprinterPrinter&) p).italicsProprinter(s);
             break;
           case STAR:
             ((StarPrinter&) p).italicsStar(s);
             break;
           default:
             cerr << "Call tech support at 1-800-BAD-BUGS\n";
         }
       }
       

      Although the example uses classes and virtual functions, it is not the best use of OO technology. The type() member function is used in basically the same way that procedural code uses tagged unions (that is, tag fields that indicate which piece of the union is currently being used). This approach is subject to error and is nonextensible compared to the proper use of classes and virtual functions, shown later in this chapter. For example, adding a new kind of printer requires changes to the printUsingItalics() function and probably to other functions as well. This is a ripple effect that is typical with non-OO (and bad OO!) software.

        В чем основная проблема динамической проверки?

        The basic problem with dynamic type checking is that it uses code to find code, creating extensibility problems later.

        With dynamic type checking, code has to be written to check the type of an object to see if it supports a particular set of member functions (this is the code that is doing the finding). Accessing the member functions may require a down cast to the appropriate pointer type (this is the code that is being searched for).

        When the user code uses code to find server code, the user code is more complex and fragile. OO programming is supposed to encapsulate complexity, and the in appropriate use of dynamic type checking can undo this benefit. Often dynamic type-checking tests require the user code to know the server's inheritance hierarchy, in which case changing the server's inheritance hierarchy breaks the user code. This is unfortunate, considering that one of the main goals of object-oriented technology is to reduce maintenance costs.

        Dynamic type checking also requires a runtime check to ensure that the object supports the requested member function. This is usually implemented using control flow, such as an if or switch statement. These runtime tests are frequently avoidable if the design exploits the static type-checking capabilities of the C++ compiler.

        Finally, it is much more expensive to catch an error at runtime than it is to find the same error at compile time. Don't use dynamic type checking without a good reason.

          Как избежать динамической проверки типов?

          Design. Design. Design.

          Circumstances sometimes require the use of dynamic type checking, but unfortunately, dynamic type checking is often used when it is not required. Often dynamic type checking is used because the programmer does not have enough expertise or does not take the time to produce a good object-oriented design. When dynamic type checking seems attractive, try revising the design instead. After the design has been revisited and dynamic type checking still seems desirable, use it. But be aware of the additional coding, testing, and maintenance costs.

            Есть ли альтернатива динамической проверке типов?

            One alternative to dynamic type checking and down casts is dynamic binding and virtual functions. To use this alternative technique, member functions that show up only in the derived classes are generalized and moved up to the base class. Effectively this means that the class selection and down cast is performed automatically and safely by C++. Furthermore, this approach produces extensible software because it automatically extends itself whenever a new derived class is createdas if an extra case or else if magically appeared in the dynamic type-checking technique.

            The following example is a rework of the code from FAQ 27.03. Compared to the old class hierarchy, the italicsXXX() member functions from the derived classes are generalized and moved into the base class as virtual member function italics(). This results in a substantial simplification of the user code printUsingItalics(). Instead of selecting the printer type based on a type() member function and using control flow logic to figure out what to do, the user code simply invokes the new italics() member function.

             class Printer {
             public:
               virtual ~Printer() throw();
               virtual void italics(const char* s) throw() = 0;
             };
             
             Printer::~Printer() throw()
             { }
             
             class EpsonPrinter : public Printer {
             public:
               virtual void italics(const char* s) throw();
             };
             
             void EpsonPrinter::italics(const char* s) throw()
             { cout << esc << "i+" << s << esc << "i-"; }
             
             class ProprinterPrinter : public Printer {
             public:
               virtual void italics(const char* s) throw();
             };
             
             void ProprinterPrinter::italics(const char* s) throw()
             { cout << esc << "[i" << s << esc << "[n"; }
             
             class StarPrinter : public Printer {
             public:
               virtual void italics(const char* s) throw();
             };
             
             void StarPrinter::italics(const char* s) throw()
             { cout << esc << "x" << s << esc << "y"; }
             
             void printUsingItalics(Printer& p, const char* s) throw()
             { p.italics(s); }
             

            From a broader standpoint, complexity is moved from the user code to the server code, from the many to the few. This is normally the right trade-off. Furthermore, adding a new kind of Printer doesn't require existing code to be modifiedreducing the ripple effect when compared to FAQ 27.03.

              FAQ 27.07 What is a capability query?

              A capability query is an inspector member function (see FAQ 14.07) that allows users to determine whether an object supports some other member function. Capability queries invite inflexibility.

              The benefit of capability queries is that they allow a class designer to avoid thinking about how users will use the objects, instead forcing the user code to explicitly test the classes and objects to see what capabilities they support.

              The problem with capability queries is that they allow a class designer to avoid thinking about how users will use the objects, instead forcing the user code to explicitly test the classes and objects to see what capabilities they support.

              Capability queries export complexity from the server to the users, from the few to the many. User code often needs explicit control flow to select operations based on the results of a capability queryuser code uses code to find code (see FAQ 27.04). This impacts existing user code when new derived classes are added.

              Capability queries are not normally recommended.

                FAQ 27.08 What is an alternative to dynamic type checking with containers?

                Templates offer a viable alternative when working with containers.

                In the past, some container classes were designed assuming the existence of some kind of master base class. This has been called the based object approach. In particular, it was common to encounter container classes that inserted or extracted elements that were pointers to a single base class, typically called Object.

                Applying the based object approach to containers makes it hard to mix two or more class libraries. For example, it may not be possible to put an object from one library into a container from another library, since the master base classes from the two libraries normally won't match exactly.

                In general, this approach can and should be avoided through the use of templates or design patterns. The particular problem of extensible container classes has been elegantly solved in the standard C++ container classes by using templates and iterators.

                Note that Java always inherits all classes from class Object. But since there is exactly one Object class in Java, as opposed to one per library vendor in C++, it isn't as big a problem in Java. The important point is that Java and C++ are very different in some fundamental ways. Syntactically they appear to be quite similar, but semantically there are some fundamental differences. Therefore just because a technique works in one language (say the based object approach works in Java) does not mean the same approach works or should be made to work in a different language (see FAQ 28.08).

                  FAQ 27.09 Are there cases where dynamic type checking is necessary?

                  Yes, particularly with persistent heterogeneous objects.

                  A program can't have static knowledge about things that existed before the execution of the program. If objects from several classes were previously stored in a database, the program that peels the objects off the disk drive's platter (or, equivalently, slurps them from a coaxial cable) cannot know the types of the objects because it didn't create them.

                  In these cases, the objects may need to be queried about their types, especially if the persistent objects are highly heterogeneous. To whatever extent possible, use the maxim "Ask once, then remember." In other words, try to avoid asking an object its type (or its capabilities) every time it is used. This is especially true if the queries require reasoning about the objects in a nonextensible manner (that is, control flow logic that uses code to find code; see FAQ 27.04).

                  Note that it is normally possible to avoid the type queries if the objects are known to be of the same class (homogeneous) or at least known to be derived from some common ABC that has a fairly rich set of member functions.

                    FAQ 27.10 Given a pointer to an ABC, how can the class of the referent be found?

                    This is an idea that should be avoided.

                    The typical reason for trying to find an object's class is to use an algorithm that depends on the object's class. If the algorithm varies depending on the derived class, then the algorithm should be a virtual member function in the class hierarchy. If the algorithm is structurally the same for all derived classes but has little pieces that differ depending on the derived class, then the little pieces should be virtual member functions in the class hierarchy. This technique lets derived classes select the ideal algorithm or algorithm fragments without any additional branch points in the software (see FAQ 27.04).

                    For example, finding the minimal distance to a mouse click requires different algorithms for circles, squares, lines, and so forth. One might be tempted to write non-OO code such as the following (pretend Position, Shape, Circle, and so forth, are classes).

                     int dist_BAD_FORM(Shape& s, Position mouse) throw()
                     {
                       Circle* cp = dynamic_cast<Circle*>(&s);
                       Square* sp = dynamic_cast<Square*>(&s);
                       Line*   lp = dynamic_cast<Line*>(&s);
                     
                       if (cp != NULL) {
                         //find the distance from mouse to the Circle, *cp
                       } else if (sp != NULL) {
                         //find the distance from mouse to the Square, *sp
                       } else if (lp != NULL) {
                         //find the distance from mouse to the Line, *lp
                       }
                     }
                     

                    One problem with this non-OO technique is that adding a new derived class requires working user code to be modified by adding a new else if section. Besides the obvious concern that changing working user code may break it, in large systems it is difficult to find all the places that need to be changed, and in very large systems there is typically a scheduling problem coordinating the changes in diverse teams of developers. In one organization the ripple effect was so bad that it took nine months to add a new gizmo to the system (this was mainly due to a scheduling concern since the entire system was hugein excess of 10 million lines of non-OO code). After a proper OO design of selected subsystems, the same sorts of additions are now routinely done by a single person in a single day.[1]

                    [1] "Lessons Learned from the OS/400 OO Project,"Communications of the ACM.1995;38(10):54 64.

                    A proper OO design would move the function dist() into the Shape rather than moving the Shape into the function dist().

                     class Position { };
                     
                     class Shape {
                     public:
                       virtual ~Shape() throw();
                       virtual void draw() const throw() = 0;
                       virtual int  dist(Position mouse) const throw() = 0;
                     };
                     
                     Shape::~Shape() throw()
                     { }
                     
                     class Circle : public Shape {
                     public:
                       virtual void draw() const throw();
                       virtual int dist(Position mouse) const throw();
                     };
                     
                     void Circle::draw() const throw()
                     { /*draw a Circle*/ }
                     
                     int Circle::dist(Position mouse) const throw()
                     { /*find the distance from mouse to this Circle*/ }
                     
                     class Square : public Shape {
                     public:
                       virtual void draw() const throw();
                       virtual int dist(Position mouse) const throw();
                     };
                     
                     void Square::draw() const throw()
                     { /*draw a Square*/ }
                     
                     int Square::dist(Position mouse) const throw()
                     { /*find the distance from mouse to this Square*/ }
                     

                    The OO solution greatly reduces the amount of code that needs to be modified when a new class is added. A little extra design work pays large dividends.

                      FAQ 27.11 What is a downcast?

                      A downcast is the conversion of a Base* to a Derived*, where class Derived is publicly derived from class Base. A downcast is used when the client code thinks (or hopes!) that a Base* points to an object of class Derived or a class derived from Derived and it needs to access a member function that is provided by Derived but not by Base.

                      For example, suppose class LiquidAsset is derived from class Asset, and LiquidAsset is a derived class that is liquidatable but Asset itself is not liquidatable. A downcast from an Asset* to a LiquidAsset* allows the liquidation.

                       #include <iostream>
                       using namespace std;
                       
                       class Asset {
                       public:
                         virtual ~Asset() throw();
                         virtual bool isLiquidatable() const throw();
                       };
                       
                       Asset::~Asset() throw()
                       { }
                       
                       bool Asset::isLiquidatable() const throw()
                       { return false; }
                       
                       class LiquidAsset : public Asset {
                       public:
                         LiquidAsset(int value=100) throw();
                         int  getValue() const throw();
                         void setValue(int value) throw();
                         virtual bool isLiquidatable() const throw();
                       protected:
                         int value_;    //value of this asset
                       };
                       
                       LiquidAsset::LiquidAsset(int value) throw()  : value_(value) { }
                       int LiquidAsset::getValue() const throw()        { return value_;  }
                       void LiquidAsset::setValue(int value) throw()    { value_ = value; }
                       bool LiquidAsset::isLiquidatable() const throw() { return true;    }
                       
                       int tryToLiquidate(Asset& asset) throw()
                       {
                         int value;
                         if (asset.isLiquidatable()) {
                           LiquidAsset& liquidAsset = (LiquidAsset&) asset;
                           value = liquidAsset.getValue();
                           liquidAsset.setValue(0);
                           cout << "Liquidated $" << value << '\n';
                         } else {
                           value = 0;
                           cout << "Sorry, couldn't liquidate this asset\n";
                         }
                         return value;
                       }
                       int main()
                       {
                         Asset       a;
                         LiquidAsset b;
                       
                         tryToLiquidate(a);
                         tryToLiquidate(b);
                       }
                       

                      The output of this program follows.

                       Sorry, couldn't liquidate this asset
                       Liquidated $100
                       

                      Although dynamic_cast (see FAQ 27.17) can eliminate the unsafe casts, it cannot eliminate the nonextensible control flow logic. See FAQ 27.12 for a better alternative.

                        FAQ 27.12 What is an alternative to using downcasts?

                        Move the user code into the object in the form of virtual functions.

                        An if-downcast pair can often be replaced by a virtual function call. The key insight is to replace the capability query with a service request. A service request is a virtual function that the client can use to politely ask an object to perform some action (such as "Try to liquidate yourself").

                        To help find the segments of code that will need to be moved into the service requests, look for those segments of code that use capability queries and depend on the type of the class. Segments of code that depend on the type of the class should be moved into the hierarchy as virtual functions; segments of code that don't depend on the type of the class can remain user code or can be nonvirtual member functions in the base class.

                        In the previous FAQ, the service request in the user code included the entire tryToLiquidate operation (this entire operation depended on the derived class). To apply this guideline, move the code for this operation into the class hierarchy as a virtual function.

                         #include <iostream>
                         using namespace std;
                         
                         class Asset {
                         public:
                           virtual ~Asset() throw();
                           virtual int tryToLiquidate() throw();
                         };
                         
                         Asset::~Asset() throw()
                         { }
                         
                         int Asset::tryToLiquidate() throw()
                         {
                           cout << "Sorry, couldn't liquidate this asset\n";
                           return 0;
                         }
                         
                         class LiquidAsset : public Asset {
                         public:
                           LiquidAsset(int value=100) throw();
                           virtual int tryToLiquidate() throw();
                         protected:
                           int value_;    //value of this asset
                         };
                         
                         LiquidAsset::LiquidAsset(int value) throw()
                         : value_(value) { }
                         
                         int LiquidAsset::tryToLiquidate() throw()
                         {
                           int value = value_;
                           value_ = 0;
                           cout << "Liquidated $" << value << '\n';
                           return value;
                         }
                         
                         int sample(Asset& asset)
                         { return asset.tryToLiquidate(); }
                         
                         int main()
                         {
                           Asset       a;
                           LiquidAsset b;
                         
                           sample(a);
                           sample(b);
                         }
                         

                        The output of this program follows.

                         Sorry, couldn't liquidate this asset
                         Liquidated $100
                         

                        In the previous FAQ, the downcast was explicit and was therefore subject to human error. In the revised solution, the conversion from Asset* to LiquidAsset* is implicitly part of the virtual function call mechanism. LiquidAsset::tryToLiquidate() does not need to downcast the this pointer into a LiquidAsset*.

                        Think of a virtual function call as an extensible if-downcast pair that always down casts to the right type.

                          FAQ 27.13 Why are downcasts dangerous?

                          Downcasts override the help a compiler can give and rely solely on the knowledge of the programmer.

                          A downcast from a base class pointer to a derived class pointer instructs the compiler to blindly reinterpret the bits of the pointer. But if you've guessed wrong about the object's class, you're in big troublethe coerced pointer can create havoc. Learn about type-safe downcasting with RTTI (see FAQ 27.16) instead, but more important, avoid downcasts entirely.

                            FAQ 27.14 Should the inheritance graph of C++ hierarchies be tall or short?

                            The inheritance graph should be a forest of short trees.

                            When the inheritance graph is too tall, downcasts are common. This is because the type of the pointer is often sufficiently different from the type of the object that the desired member function is available only by downcasting the pointer. Also, the deeper the graph, the less likely that the inheritance relationships are proper. A tall graph is frequently a sign of an uninformed attempt at code reuse. Remember: inheritance is not for code reuse (see FAQ 8.12).

                            The type-safe philosophy espoused in this book discourages the unnecessary use of downcasting, even if downcasts are checked first.

                              FAQ 27.15 Should the inheritance graph of C++ hierarchies be monolithic or a forest?

                              The inheritance graph should be a forest.

                              The inheritance hierarchy of well-designed C++ software is normally a forest of little trees rather than a large, monolithic tree. Monolithic trees usually result in excessive use of downcasting. The type-safe philosophy espoused in this book discourages the use of downcasting.

                                FAQ 27.16 What is Runtime Type Identification (RTTI)?

                                RTTI is the official way in standard C++ to discover the type of an object and to convert the type of a pointer or reference (that is, dynamic typing). The need came from practical experience with C++. RTTI replaces many homegrown versions with a solid, consistent approach. It has many features and capabilities; this chapter discusses dynamic_cast<T>(), static_cast<T>(), and typeid(). Other features, such as const_cast() and reinterpret_cast(), and issues related to multiple/private/protected/virtual inheritance are not discussed.

                                  FAQ 27.17 What is the purpose of dynamic_cast<T>()?

                                  It's a way to see if an object supports a given interface at runtime. It can be a bit complicated, so this simplified FAQ covers only the normal situations that occur repeatedly.

                                  Very loosely speaking, dynamic_cast<T>(x) is like the old-style cast (T)x, meaning that it casts the value of x to the type T (T is normally either a pointer or a reference to some class). dynamic_cast<T>(x) has several important advantages over the old-style cast. It never performs an invalid conversion since it checks that the cast is legal at runtime, and the syntax is more obvious and explicit than the old-style cast, thus appropriately calling attention to the conversion.

                                  If p is a pointer, dynamic_cast<Fred*>(p) converts p to a Fred* like (Fred*)p, but if the conversion is not valid, it returns NULL. If r is a reference, dynamic_cast<Fred&>(r) converts r to a Fred& just like (Fred&)r, but if the conversion is not valid, an exception of type bad_cast is thrown. A conversion is valid if the object pointed to by p (or referred to by r) is either a Fred or a publicly derived class of Fred. Here is some sample syntax.

                                   #include <iostream>
                                   using namespace std;
                                   
                                   class Shape {
                                   public:
                                     virtual ~Shape() throw();
                                     // ...
                                   };
                                   
                                   Shape::~Shape() throw() { }
                                   
                                   class Circle : public Shape { /*...*/ };
                                   class Square : public Shape { /*...*/ };
                                   
                                   void sample(Shape* p) throw()
                                   {
                                     Circle* cp = dynamic_cast<Circle*>(p);
                                     if (cp != NULL) {
                                       cout << "The object is a Circle\n";
                                     } else {
                                       cout << "The object is not a Circle\n";
                                     }
                                   }
                                   
                                   int main()
                                   {
                                     Circle c;
                                     Square s;
                                     sample(&c);
                                     sample(&s);
                                   }
                                   

                                  When dynamic_cast<T>(p) is being used to perform a downcast, p's type must designate a class with at least one virtual function (or be NULL). However, this restriction does not apply to potential recipients of the cast, such as cp in the example.

                                    FAQ 27.18 Is dynamic_cast<T>() a panacea?

                                    No, like everything else, dynamic_cast<T>() can be misused.

                                    It's a horrible design error, but some programmers (mis)use dynamic_cast<T>() in huge if / then / else blocks to determine an object's type and then take the appropriate action. This situation screams out for virtual functions and dynamic binding, not the extensibility-killing misuse of RTTI (see FAQ 27.03).

                                    Also, watch out for performance hits due to this implementation technique. It is all too easy to think of dynamic_cast<T>() as a constant-time operation, when in fact it may take linear time and chew up CPU cycles if the inheritance hierarchies are deep or if the advice about huge if blocks has been ignored.

                                      FAQ 27.19 What does static_cast<T>() do?

                                      It tells the compiler, "Trust me."

                                      Sometimes the programmer knows the type of an object and has to or wants to let the compiler in on the secret. static_cast<T>() is the standard C++ way to do this at compile time. There are situations where either the knowledge to make the cast exists only in the programmer's mind or the runtime system cannot do the job because of technical reasons. Here is some sample syntax.

                                       Target* tg = static_cast<Target*>(src);  // just do it
                                       

                                      The C++ static_cast<T>() is better than C-style casting because it stands out in the code and explicitly states the programmer's understanding and intentions. It also understands and respects const and access controls.

                                        FAQ 27.20 What does typeid() do?

                                        It determines the precise type of an object at runtime.

                                        Given a reference or pointer as input, typeid() returns a reference to a standard library class called type_info. type_info has a name() member function that returns the name of the parameter's type in an implementation-specific format. This name represents the precise, lowest-level type of the object. If the value of the pointer is NULL, typeid() throws a bad_typeid exception.

                                        Note that dynamic_cast<T>(p) and static_cast<T>(p) are template functions, where T is the template parameter and p is the function parameter, but typeid() is not a template function.

                                        typeid() and dynamic_cast<T>() are two sides of the same coin. They both take a base class pointer or reference that may refer to a derived class object. But typeid() returns a class name whereas dynamic_cast<T>() is passed a class name. typeid() is used to discover the object's exact class, but it doesn't convert the pointer; dynamic_cast<T>() converts the pointer but doesn't determine the object's exact classthe pointer may be converted to some intermediate base class rather than to the object's exact class.

                                        The character representation of the class name from name() is stored in system memory and must not be deleted by the programmer.

                                          FAQ 27.21 Are there any hidden costs for type-safe downcasts?

                                          Yes, type-safe downcasts have five hidden costs.

                                          Although type-safe downcasts never cast a pointer to an incorrect type, they have five hidden costs. They increase coding cost, maintenance cost, testing cost, runtime CPU cost, and extensibility cost.

                                          1. Coding cost: Type-safe downcasts move complexity from the server code into the user code, from the few to the many.

                                          2. Maintenance cost: Moving code from the server code to the user code increases the overall software bulk.

                                          3. Testing cost: A test harness must be devised to exercise every if, including the ifs used to test the type safety of the downcasts.

                                          4. Runtime CPU cost: Additional code must be executed to test the type safety of the downcasts. This is not a constant time cost, by the way, since it may be necessary to search an entire inheritance hierarchy.

                                          5. Extensibility cost: The additional control flow code needs to be modified when new derived classes are added.

                                          The underlying cause for these costs lies with the style of programming implied by type-safe downcasts rather than with the downcasts themselves. Embracing the more extensible style of programming that does not use unnecessary downcasts is part of using C++ properly.

                                            FAQ 28.01 What are container classes and what are the most common mistakes made with container classes?

                                            Containers are objects that hold other objects (see FAQ 2.15). For example, a linked list of string objects is a containerusers can add numerous elements (or element objects) to the list, and in this case all the elements are of type string. The closest that the core C++ language comes to container classes is the arraybut see FAQ 28.02 to find out why C++ arrays should not be used.

                                            Unfortunately programmers' instincts tend to lead them in the wrong direction with containers. This may be because of past experience with other programming languages or it may be for some other reason, but whatever the reason the result is the same: programmers tend to make common and serious mistakes with containers that lead to bugs. Here are a few common mistakes.

                                            • Using arrays rather than safe container classes (the "Grandma used arrays" fiasco; see FAQ 28.02)

                                            • Rolling your own container classes from scratch (the "not invented here" fiasco; see FAQ 28.03)

                                            • Containers of pointers (the "random object ownership" fiasco; see FAQ 28.04)

                                            • Containers of char* (the "string contents vs. string address" fiasco; see FAQ 28.06)

                                            • Containers of auto_ptr<T> (the "transfer of object ownership" fiasco; see FAQ 28.07)

                                            • Inheriting everything from Object (the "based object" fiasco; see FAQ 28.08)

                                            • Selecting incompatible vendors for container classes (the "best of breed" fiasco; see FAQ 28.10)

                                              FAQ 28.02 Are arrays good or evil?

                                              Arrays are evil. They have their place in some specialized applications, but they should be avoided.

                                              The most common mistake with container classes is failing to use them and substituting arrays instead. Unfortunately, this is not only a fairly common mistake, it's also a very dangerous mistake. Arrays simply aren't safe. They're pointers in disguise, and pointers aren't safe. Yes, Grandma used pointers and survived, but given the relative size and complexity of today's software systems and given all the other things that need to be handled, raw pointers or arrays generally aren't worth the risk. Instead pointers and arrays should be wrapped inside a safe interfacea container class.

                                              Avoid using arrays and pointers. Use container classes instead.

                                                FAQ 28.03 Should application development organizations create their own container classes?

                                                No. Too many application development organizations roll their own container classes from scratch. This is often motivated by the "not invented here" (NIH) syndrome, which is lethal to any kind of reuse.

                                                The problems with application development organizations rolling their own container classes include the following.

                                                • Increased development costs: It's normally far cheaper to pay for a class library than to pay yourself to write your own.

                                                • Increased long-term costs: Building your own means increasing your maintenance costs, which is usually the last thing you want to do.

                                                • Degraded quality: Application developers are not usually experts in the intricacies of data structures, so they often do a mediocre job with container classesreinventing the wheel is bad enough, but usually they reinvent a flat tire.

                                                • Lack of flexibility and versatility: It is hard for an in-house development team to compete with the well-funded software houses in developing software that is flexible enough to meet the needs of the whole industry. This flexibility may be very important later in the life cycle.

                                                • Loss of focus: Application developers should focus on getting the application done on time and within budget, not on plumbing.

                                                • Missed opportunities for standardization: It makes sense for the containers to be as standardized as the basic language types for both training and maintenance considerations.

                                                  FAQ 28.04 What are some common mistakes with containers of pointers?

                                                  Lifetime errors caused by ownership confusion.

                                                  Be careful about containers of pointers. A container of pointers contains pointers to the objects that are inserted into the container, whereas a container of values contains copies of the objects that are inserted into the container. The main purpose of a container of pointers is to hold on to the identities of certain objects, but the main purpose of a container of values is to hold on to the state of certain objects.

                                                  Containers of pointers allow objects to be inserted without copying, allow the contained objects to be distinguished by identity rather than merely by state or value, and allow objects of classes within an inheritance lattice to be inserted without slicing (a.k.a. chopped copies). However, containers of pointers can create a difficult relationship between the user of the container and the container. For example, if a pointer to an object is in some container and someone else deletes the object, the container ends up with a dangling reference (see FAQ 24.04). In some cases this means that an object needs to know all the containers that point to it, which sometimes requires each object to maintain a container of container pointers. Sometimes users can't even change an object without informing all the containers that point to the object. For example, if a user changes an object's state and a container's order semantics depend on the object's state, the innocent change might subtly break the container's invariant. This becomes very messy and complex.

                                                    FAQ 28.05 Does this mean that containers of pointers should be avoided?

                                                    The rule of thumb is to use containers of values when you can, use containers of pointers when you must.

                                                    Containers of pointers must be used when the identity of the contained objects matters, in addition to their state. For example, containers of pointers must be used when the same instance must be "in" several containers at the same time.

                                                    Another reason containers of pointers are used is when the cost to copy an object into and out of a container is too large, but in this case judicious use of reference counting (see FAQ 31.09) and copy on write (see FAQ 31.10) can reduce the copying cost significantly.

                                                      FAQ 28.06 Surely good old-fashioned char* is an exception, right?

                                                      Wrong!

                                                      For example, if a container of char* is used when the goal is to have a container of strings, lookup requests may fail since the container will be looking for a particular pointer rather than for the contents of the string. To make matters worse, if a string buffer is used to build the string foo and that is inserted into the container and the string buffer is later changed to junk, the container will appear to contain junk rather than foo.

                                                      If a string is desired, a string class should be used and char* should be avoided. Every compiler vendor has a string class, and there is a standard C++ string class called string.

                                                        FAQ 28.07 Can auto_ptr<T> simplify ownership problems with containers of pointers?

                                                        No, auto_ptr<T> usually makes things worse!

                                                        Generally speaking, auto_ptr<T> should not be used inside container classes. For example, if a List of Fred pointers is desired, it is usually bad to use a List<auto_ptr<Fred> >. The reason is that copying an auto_ptr<T> makes changes to the original, in addition to the obvious changes to the copy. In particular, the ownership of the referent is transferred to the copy, so the original will be NULL. For example, if someone retrieved a copy of an element in the List, that would copy the auto_ptr<T>, which would transfer ownership of the referent to the copy and the auto_ptr<T> in the List would be NULL.

                                                          FAQ 28.08 Can a Java-like Object class simplify containers in C++?

                                                          No, that usually makes things worse!

                                                          Java is not C++. C++ is not Java. Just because something works well in Java doesn't mean it will work well in C++ or vice versa. Java has automatic garbage collection, C++ has destructors (FAQ 2.20); Java has a ubiquitous base class Object, C++ has templates (FAQ 2.15). For a lot of reasons, Java's container classes all take Object pointers, and they work well. It's possible to do the same thing in C++, but in C++ this technique tends to be more complex and expensive than using templates. For example, when Fred objects are inserted into a list of Object*, the only thing that is known about them is that they are some kind of Object. When these objects are accessed from the list, the programmer has to carefully find out what the objects really are and typically has to downcast the Object* before anything useful can be done with the objects. This is another form of dynamic type checking, and it can be expensive to write, test, and maintain (see FAQ 27.03).

                                                          Forcing things to inherit from a common base class such as Object is called the based object approach. See the next FAQ for more details on these heterogeneous containers.

                                                            FAQ 28.09 What's the difference between a homogeneous and a heterogeneous container?

                                                            The elements of a homogeneous container are all of the same type; the elements of a heterogeneous container can be of different types.

                                                            Containers come in many shades of gray. Generally speaking, the more heterogeneous the element types are, the less type safe the container is. For example, the ultimate heterogeneous container is a container of void* in which the various elements could point to objects of any type. Even though this seems to optimize flexibility, in practice such containers are nearly worthless. In particular, putting things into such a container is easy (any object of any type can be inserted), but when an element is removed, the user of the container knows nothing about the element's type.

                                                            A more useful form of heterogeneous container is one that requires its elements to point to objects derived from some specific base class, often an ABC. This base class typically has all or nearly all the member functions provided by the actual objects referenced by the container. For example, one might have a list of Shape* where the actual referents might be objects of class Square, Circle, Hexagon, and so on.

                                                            Note that the based object approach (see the previous FAQ) is another form of heterogeneous container.

                                                              FAQ 28.10 Is it a good idea to use a "best of breed" approach when selecting container classes?

                                                              Not usually.

                                                              For small projects, it's often desirable to select each component using a best of breed approach. That is, select the best of breed in category X, the best of breed in category Y, and so on.

                                                              But for large, mission-critical projects, it is very important to reduce the overall system complexity, and selecting the very best container classes could actually make things worse (see also FAQ 39.08). Most vendors' GUI frameworks are designed to work very well with the vendor's container classes. In some cases, this increases the integration costs of mixing container classes from one vendor and GUI classes from another. Database, network, and other infrastructure frameworks are similar: it is often difficult and/or risky to mix libraries and frameworks classes from different vendors.

                                                              When this mix-and-match problem occurs, the low-cost, low-risk approach is often to select the container classes that "go with" the rest of the infrastructure. Usually that means that the container classes are less than ideal, often frustrating programmers who don't see the big picture. But remember: the goal is to reduce the overall system complexity, and container classes are only one piece.

                                                                FAQ 28.11 Should all projects use C++'s standardized containers?

                                                                Not always, because of big picture issues. We hope that someday the software industry will fully embrace standardized containers, but until then, be prepared to make decisions that will be uncomfortable.

                                                                When compared to other container classes, C++'s standardized containers have several benefits.

                                                                • They are standardized and are therefore portable.

                                                                • They are fastvery, very fast.

                                                                However in large and/or mission-critical applications, integration issues often dominate. When this happens, it may be better in the overall scheme of things to use a vendor's proprietary container classes (see FAQ 28.10). Programmers who have not worked on large and/or mission-critical projects where integration issues became a problem do not understand this and usually argue in favor of the portability or performance of the standardized container classes.

                                                                For example, in some cases a particular vendor's library is considered essential to the project's success, and the library might be (shouldn't be, but in the real world it might be) tightly integrated with the container classes from the same vendor. In such a case, the cost and risk of integrating standard containers might not be worth the benefit. It's a dilemma of local versus global optimization.

                                                                Of course there are some cases where performance or portability is more important than integration. In these cases the standardized C++ container classes should be considered, even if integrating them with the other third-party libraries requires some invention.

                                                                  FAQ 28.12 What are the C++ standardized container classes?

                                                                  The C++ container classes are a part of what was formerly known as the standard template library (STL). They include the following container classes.

                                                                    FAQ 28.13 What are the best applications for the standardized C++ sequence container classes?

                                                                    Operations that make sense for a linear sequence of objects.

                                                                    The sequence container classes store their objects in a linear sequence. They include the following container classes.

                                                                    • vector<T>

                                                                    • list<T>

                                                                    • deque<T>

                                                                    They all expand themselves to allow an arbitrary number of elements to be inserted, and all provide numerous operations. The biggest difference between them is related to performance. For example, vector<T> has very fast array-like access to random elements and very fast insertions and removals at the end; list<T> is not as fast to access a random element, but it is faster when inserting in the middle or at the beginning; deque<T> is like vector<T> except that it also has very fast insertion and removal of elements at the beginning.

                                                                    Some of the operations that can be performed on a sequence follow. In the following descriptions, first and toofar represent iterators within a container, and "the range [first, toofar)" means all the elements starting at first up to but not including the element at toofar.

                                                                    Selected nonmutating sequence operations:

                                                                    • for_each(first, toofar, function) applies the function to each element in the range [first, toofar).

                                                                    • find(first, toofar, const T& value) finds the first element that is equal to value in the range [first, toofar), returning the corresponding iterator (or toofar if nothing matched). find_if() is similar, but it takes a predicate to determine if the element matches.

                                                                    • adjacent_find(first, toofar) finds the first adjacent pair of elements that are equal in the range [first, toofar), returning the corresponding iterator (or toofar if nothing matched). An optional binary predicate can be given to determine if an adjacent pair matches.

                                                                    • count(first, toofar, const T& value, result) counts the number of elements in the range [first, toofar) that are equal to value and stores that number in the by-reference parameter result. count_if() is similar, but it takes a predicate to determine if the element matches.

                                                                    • mismatch(first, toofar, first2) finds the first mismatch between the two sequences. The elements from sequence 1 are in the range [first, toofar), and the corresponding elements from sequence 2 start at first2. An optional binary predicate can be used to determine if elements match.

                                                                    • equal(first, toofar, first2) checks two sequences for element-by-element equality. It returns true if and only if the elements of sequence 1 in the range [first, toofar) match the corresponding elements of sequence 2 starting at first2. An optional binary predicate can be used to determine if elements match.

                                                                    • search(first, toofar, first2, toofar2) finds a subsequence within a sequence. An optional binary predicate can be given to determine if an adjacent pair matches.

                                                                    Selected mutating sequence operations:

                                                                    • copy(first, toofar, dest) copies elements from the range [first, toofar) into the sequence starting at dest. copy_backward() is similar, but it copies elements from right to left, which is especially useful when sliding a sequence a few slots to the right (that is, when the source and destination ranges overlap).

                                                                    • swap_ranges(first, toofar, first2) swaps the contents of the elements from range [first, toofar) with the corresponding elements from sequence 2 starting at first2.

                                                                    • swap(a, b) swaps the values of the elements or the entire sequences.

                                                                    • transform(first, toofar, dest, unaryOp) calls unaryOp() on each element in the range [first, toofar) and copies the results into sequence 2 starting at dest.

                                                                    • transform(first, toofar, first2, dest, binaryOp) calls binaryOp(), passing an element from sequence 1 in the range [first, toofar) and the corresponding element from sequence 2 starting at first2, and copies the results into sequence 3 starting at dest.

                                                                    • replace(first, toofar, const T& oldValue, const T& newValue) replaces all elements equal to oldValue in the range [first, toofar) with newValue. replace_if() is simliar, but it takes a predicate to determine if the element matches. replace_copy() and replace_copy_if() are similar except that the original sequence is unchanged, and the results are copied to a second sequence.

                                                                    • fill(first, toofar, const T& value) fills the range [first, toofar) with copies of value. fill_n() is similar, but it takes a starting iterator and a number.

                                                                    • generate(first, toofar, generator) fills the range [first, toofar) with the results of successively calling generator(). generate_n() is similar, but it takes a starting iterator and a number.

                                                                    • remove(first, toofar, const T& value) removes those elements from the range [first, toofar) that are equal to value. remove_if() is similar, but it takes a predicate to determine if the element matches. remove_copy() and remove_copy_if() are similar except that the original sequence is unchanged, and the results are copied to a second sequence.

                                                                    • unique(first, toofar) removes successive duplicates from the range [first, toofar). An optional binary predicate can be given to determine if an adjacent pair matches. unique_copy() is similar except that the original sequence is unchanged, and the results are copied to a second sequence.

                                                                    • reverse(first, toofar) reverses the elements of the range [first, toofar). reverse_copy() is similar except that the original sequence is unchanged, and the results are copied to a second sequence.

                                                                    • rotate(first, middle, toofar) rotates the range [first, toofar) around the iterator middle. rotate_copy() is similar except that the original sequence is unchanged, and the results are copied to a second sequence.

                                                                    • random_shuffle(first, toofar) randomly shuffles the elements in the range [first, toofar).

                                                                    • partition(first, toofar, unaryOp) partitions the range [first, toofar) by moving elements where unaryOp() returns true to the left, the others to the right. stable_partition() is similar, but it preserves the relative order of the elements within each group.

                                                                    Selected sorting and related operations:

                                                                    • All the operations in this section take an optional binary predicate used to compare two elements.

                                                                    • sort(first, toofar) sorts the elements in the range [first, toofar).

                                                                    • stable_sort(first, toofar) sorts the elements in the range [first, toofar) and never reverses two elements that compare as equal.

                                                                    • binary_search(first, toofar, const T& value) looks for value in the sorted sequence range [first, toofar) and returns true or false based on whether value was found.

                                                                    • includes(first, toofar, first2, toofar2) checks if the first multiset [first, toofar) is a subset of the second multiset [first2, toofar2).

                                                                    • set_union(first, toofar, first2, toofar2, dest) copies into dest the union of the first set [first, toofar) and the second set [first2, toofar2).

                                                                    • set_intersection(first, toofar, first2, toofar2, dest) copies into dest the intersection of the first set [first, toofar) and the second set [first2, toofar2).

                                                                    • set_difference(first, toofar, first2, toofar2, dest) copies into dest the difference between the first set [first, toofar) and the second set [first2, toofar2).

                                                                    • min_element(first, toofar) finds the minimum element within the range [first, toofar). max_element() is analogous.

                                                                      FAQ 28.14 What are the best situations for the standardized C++ associative container classes?

                                                                      Operations that make sense for keyed containers of objects.

                                                                      The associative container classes store their objects so that the keys are in a sorted order based on a comparison object or function. They include the following container classes.

                                                                      • set<T>

                                                                      • multiset<T>

                                                                      • map<Key,Value>

                                                                      • multimap<Key,Value>

                                                                      They all expand to allow an arbitrary number of elements to be inserted, and all provide numerous operations. set<T> and multiset<T> allow the insertion, lookup, and removal of keys, with multiset<T> allowing multiple copies of the same key value (some libraries use the term "bag" for this). map<Key,Value> and multimap<Key,Value> allow values to be inserted, looked up, and removed by their key, with multiset<T> allowing multiple values associated with the same key value.

                                                                      Here are some selected ways to use the standard map<Key,Value> class.

                                                                       #include <string>
                                                                       #include <map>
                                                                       #include <iostream>
                                                                       using namespace std;
                                                                       
                                                                       typedef  map<string,int>  AgeMap;                    <-- 1
                                                                       
                                                                       int main()
                                                                       {
                                                                         AgeMap age;
                                                                       
                                                                         // The subscript operator is used to access an element in the map
                                                                         age["Fred"] = 42;
                                                                         age["Barney"] = 38;
                                                                         int n = age["Fred"];
                                                                         int numElements = age.size();
                                                                       
                                                                         // Removing an element from the map
                                                                         age.erase("Barney");
                                                                       
                                                                         // Iterators are used to loop through all the elements within the map
                                                                         for (AgeMap::iterator p = age.begin(); p != age.end(); ++p)
                                                                           cout << "age of " << p->first << " is " << p->second << "\n";
                                                                       
                                                                         // Iterators can be used to check if an entry is in the map
                                                                         AgeMap::const_iterator q = age.find("Fred");
                                                                         if (q == age.end())
                                                                           cout << "Fred isn't in the map\n";
                                                                         else
                                                                           cout << "Fred is in the map; his age is " << q->second << "\n";
                                                                       }
                                                                       

                                                                      (1) A typedef can make the rest easier to read

                                                                        FAQ 29.01 What is the difference between overloaded functions and overridden functions?

                                                                        Overloading has the same scope, same name, different signatures, and virtual is not required. Overriding has different scopes, same name, same signatures, and virtual is required.

                                                                        The term signature designates the combination of a function's name, the types and order of its parameters, and, if the function is a nonstatic member function, its const and/or volatile qualifiers.

                                                                        Overloading occurs when two or more functions are in the same scope (for example, both in the same class or both at namespace scope) and have the same name but different signatures. Overriding occurs when a class and one of its derived classes both define a member function with the same signature and the member function is declared to be virtual in the base class.

                                                                        In the following example, Base::f(int) and Base::f(float) overload each other, while Derived::g() overrides Base::g().

                                                                         #include <iostream>
                                                                         using namespace std;
                                                                         class Base {
                                                                         public:
                                                                           virtual ~Base()         throw();
                                                                           virtual void f(int x)   throw();
                                                                           virtual void f(float x) throw();
                                                                           virtual void g()        throw();
                                                                         };
                                                                         
                                                                         Base::~Base() throw()
                                                                           { }
                                                                         void Base::f(int x) throw()
                                                                           { cout << "Base::f(int)\n"; }
                                                                         void Base::f(float x) throw()
                                                                           { cout << "Base::f(float)\n"; }
                                                                         void Base::g() throw()
                                                                           { cout << "Base::g()\n"; }
                                                                         
                                                                         class Derived : public Base {
                                                                         public:
                                                                           virtual void g() throw();
                                                                         };
                                                                         
                                                                         void Derived::g() throw()
                                                                           { cout << "Derived::g()\n"; }
                                                                         
                                                                         int main()
                                                                         {
                                                                           Derived d;
                                                                           Base* bp = &d;  // OK: Derived is kind-of Base
                                                                           bp->f(42);
                                                                           bp->f(3.14f);
                                                                           bp->g();
                                                                         }
                                                                         

                                                                        The output of this program follows.

                                                                         Base::f(int)
                                                                         Base::f(float)
                                                                         Derived::g()
                                                                         

                                                                          FAQ 29.02 What is the hiding rule?

                                                                          A rule in C++ that tends to confuse new C++ developers.

                                                                          The hiding rule says that an entity in an inner scope hides things with the same name in an outer scope. And since a class is a scope, this means that a member of a derived class hides a member of a base class that has the same name as the derived class member. Confused? Don't give up; this is really important stuff.

                                                                          There are two common situations when the hiding rule confuses people. First, when a base class and a derived class declare member functions with different signatures but with the same name, then the base class member function is hidden. Second, when a base class declares a nonvirtual member function and a derived class declares a member function with the same signature, then the base class member function is hidden (technically the same thing happens with virtual member functions, but in that case it hardly ever confuses people).

                                                                          In the following example, Base::f(float) and Base::g(float) are virtual and therefore can be overridden by derived classes, but Base::h(float) is nonvirtual and therefore should not be redefined in derived classes.

                                                                           #include <iostream>
                                                                           using namespace std;
                                                                           
                                                                           class Base {
                                                                           public:
                                                                             virtual ~Base()         throw();
                                                                             virtual void f(float x) throw();
                                                                             virtual void g(float x) throw();
                                                                                     void h(float x) throw();
                                                                           };
                                                                           
                                                                           Base::~Base() throw()
                                                                             { }
                                                                           void Base::f(float x) throw()
                                                                             { cout << "Base::f(float)\n"; }
                                                                           void Base::g(float x) throw()
                                                                             { cout << "Base::g(float)\n"; }
                                                                           void Base::h(float x) throw()
                                                                             { cout << "Base::h(float)\n"; }
                                                                           

                                                                          In the following code, member function Derived::f(float) is a normal override of virtual Base::f(float). However, Derived::g(int) hides (rather than overrides or overloads) Base::g(float) and Derived::h(float) hides (rather than overrides or overloads) Base::h(float).

                                                                           class Derived : public Base {
                                                                           public:
                                                                             virtual void f(float x) throw();                   <-- 1
                                                                             virtual void g(int x)   throw();                   <-- 2
                                                                                     void h(float x) throw();                   <-- 3
                                                                           };
                                                                           void Derived::f(float x) throw()
                                                                             { cout << "Derived::f(float)\n"; }
                                                                           void Derived::g(int x) throw()
                                                                             { cout << "Derived::g(int)\n"; }
                                                                           void Derived::h(float x) throw()
                                                                             { cout << "Derived::h(float)\n"; }
                                                                           

                                                                          (1) Good: Overrides Base::f(float)

                                                                          (2) Bad: Hides Base::g(float)

                                                                          (3) Bad: Redefines a nonvirtual function

                                                                          Because Derived::f(float) is a normal override of Base::f(float), calling f(3.14f) on a Derived object does the same thing independent of whether the reference to the Derived object is of type Base& or type Derived&. Said simply, the behavior depends on the type of the object, not on the type of the reference. This is the normal (and desirable) effect of dynamic binding, and it is shown in sampleOne().

                                                                           void sampleOne(Base& b, Derived& d)
                                                                           {
                                                                             b.f(3.14f);                                        <-- 1
                                                                             d.f(3.14f);
                                                                           }
                                                                           
                                                                           int main()
                                                                           {
                                                                             Derived d;
                                                                             sampleOne(d, d);
                                                                           }
                                                                           

                                                                          (1) Good: If the object is a Derived, calls Derived::f(float)

                                                                          Unfortunately, Derived::g(int) neither overrides nor overloads Base:: g(float) but rather hides Base::g(float). Therefore the compiler calls g(float) if someone tries to call g(int) on a Derived&. This behavior is surprising to many developers; it is shown in sampleTwo().

                                                                           void sampleTwo(Base& b, Derived& d)
                                                                           {
                                                                             b.g(3.14f);
                                                                             d.g(3.14f);                                        <-- 1
                                                                           }
                                                                           
                                                                           int main()
                                                                           {
                                                                             Derived d;
                                                                             sampleTwo(d, d);
                                                                           }
                                                                           

                                                                          (1) Bad: Converts 3.14 to 3 and calls Derived::g(int)

                                                                          Also unfortunately, Derived::h(float) is a redefinition of the nonvirtual function Base::h(float). Since Base::h(float) is nonvirtual, Derived:: h(float) is not an override, and dynamic binding does not occur. Therefore, the compiler calls Base::h(float) if someone tries to call h(float) on a Derived object using a Base&. This behavior is surprising to many developers; it is shown in sampleThree().

                                                                           void sampleThree(Base& b, Derived& d)
                                                                           {
                                                                             b.h(3.14f);                                        <-- 1
                                                                             d.h(3.14f);
                                                                           }
                                                                           
                                                                           int main()
                                                                           {
                                                                             Derived d;
                                                                             sampleThree(d, d);
                                                                           }
                                                                           

                                                                          (1) Bad: Calls Base::h(float)does not use dynamic binding

                                                                          The root problem with sampleTwo() and sampleThree() is that the behavior depends on the type of the reference rather than on the type of the object. For example, in sampleThree() the member function that gets invoked is the one associated with the reference's type, not the one associated with the object's type. These behaviors surprise users, since users normally expect behavior to depend on the type of the object rather than on the type of the reference or pointer used to access that object.

                                                                          The hiding rule may not seem intuitive, but it prevents worse errors, especially in the case of assignment operators. If, for example, the hiding rule were removed, it would be legal to assign a Circle with a Square (the Shape part of the Square would be copied into the Shape part of the Circle).

                                                                            FAQ 29.03 How should the hiding rule be handled?

                                                                            Avoid triggering the hiding rule when possible, and use the following work-arounds when the hiding rule can't be avoided.

                                                                            Avoid hiding inherited public: member functions whenever possible. When it cannot be avoided, it is important not to surprise the class's users. The guiding principle is to avoid confusing users: when a Base* can be used to call a member function on a Derived object, calling it via a Derived* shouldn't alter the observable behavior.

                                                                            In the case of redefining a nonvirtual member function, as in Base::h(float) from the previous FAQ, the simplest way to avoid surprising users is to use the virtual keyword when declaring the base class member function. In those rare cases where the base class function cannot be virtual, ensure that the observable behavior of the derived class function is identical to that of the base class.

                                                                            For example, an experienced C++ programmer might use a nonvirtual member function to avoid the (small) overhead of a virtual function call, yet might also redefine that member function in a derived class to make better use of the derived class's resources. To avoid surprising users, there must not be any differences in the observable behavior of the two functions. Note: These relationships are somewhat subtle; if the code will be maintained by less experienced programmers, a normal, virtual function is probably a better choice.

                                                                            In the case where a base class and a derived class declare member functions with the same name but different signatures, as in Base::g(float) and Derived::g(int) in the previous FAQ, a using declaration (FAQ 29.04) should be employed.

                                                                            The following shows how these guidelines can be applied to the example from the previous FAQ.

                                                                             #include <iostream>
                                                                             using namespace std;
                                                                             
                                                                             class Base {
                                                                             public:
                                                                               virtual ~Base()         throw();
                                                                               virtual void f(float x) throw();
                                                                               virtual void g(float x) throw();
                                                                               virtual void h(float x) throw();
                                                                             };
                                                                             
                                                                             Base::~Base() throw()
                                                                               { }
                                                                             void Base::f(float x) throw()
                                                                               { cout << "Base::f(float)\n"; }
                                                                             void Base::g(float x) throw()
                                                                               { cout << "Base::g(float)\n"; }
                                                                             void Base::h(float x) throw()
                                                                               { cout << "Base::h(float)\n"; }
                                                                             
                                                                             class Derived : public Base {
                                                                             public:
                                                                               virtual void f(float x) throw();
                                                                               virtual void g(int x)   throw();                   <-- 1
                                                                               using Base::g;                                     <-- 2
                                                                             };
                                                                             
                                                                             void Derived::f(float x) throw()
                                                                               { cout << "Derived::f(float)\n"; }
                                                                             void Derived::g(int x) throw()
                                                                               { cout << "Derived::g(int)\n"; }
                                                                             

                                                                            (1) Normally this would hide g(float) (bad!)

                                                                            (2) But this line unhides g(float) (good!)

                                                                            After applying these fixes, users are not confused because the behavior depends on the type of the object rather than on the type of the pointer used to access that object.

                                                                             void sample(Base& b, Derived& d)
                                                                             {
                                                                               b.f(3.14f);
                                                                               d.f(3.14f);
                                                                             
                                                                               b.g(3.14f);
                                                                               d.g(3.14f);                                        <-- 1
                                                                             
                                                                               b.h(3.14f);
                                                                               d.h(3.14f);
                                                                             }
                                                                             
                                                                             int main()
                                                                             {
                                                                               Derived d;
                                                                               sample(d, d);
                                                                             }
                                                                             

                                                                            (1) This is not hidden (good!)

                                                                            The output of this program demonstrates that the behavior depends on the type of the object, not the type of the reference:

                                                                             Derived::f(float)
                                                                             Derived::f(float)
                                                                             Derived::g(float)
                                                                             Derived::g(float)
                                                                             Derived::h(float)
                                                                             Derived::h(float)
                                                                             

                                                                            These guidelines apply only to public inheritance; hiding base class member functions is fine for private or protected inheritance (see FAQ 37.01).

                                                                              FAQ 29.04 What should a derived class do when it redefines some but not all of a set of overloaded member functions inherited from the base class?

                                                                              The derived class should use the using syntax.

                                                                              If the base class has an overloaded set of member functions and the derived class overrides some but not all of that set, the redefined member functions will hide the other overloads. The work-around is to use the using syntax. The following example shows class Base with two overloaded member functions called f.

                                                                               #include <iostream>
                                                                               using namespace std;
                                                                               
                                                                               class Base {
                                                                               public:
                                                                                 virtual ~Base()         throw();
                                                                                 virtual void f(int x)   throw();
                                                                                 virtual void f(float x) throw();
                                                                               };
                                                                               
                                                                               Base::~Base() throw()
                                                                               { }
                                                                               
                                                                               void Base::f(int x) throw()
                                                                               { cout << "Base::f(int)\n"; }
                                                                               
                                                                               void Base::f(float x) throw()
                                                                               { cout << "Base::f(float)\n"; }
                                                                               

                                                                              Now suppose the author of class Derived wants to override one of the two f() member functions. In this case the derived class should also say using Base::f; to avoid confusing users:

                                                                               class Derived : public Base {
                                                                               public:
                                                                                 virtual void f(int x) throw();
                                                                                 using Base::f;                                     <-- 1
                                                                               };
                                                                               
                                                                               void Derived::f(int x) throw()
                                                                               { cout << "Derived::f(int)\n"; }
                                                                               

                                                                              (1) This unhides f(float) (good!)

                                                                              Because of the using Base::f; line in the derived class, f(float) is not hidden:

                                                                               void sample(Base& b, Derived& d)
                                                                               {
                                                                                 b.f(42);
                                                                                 d.f(42);
                                                                                 b.f(3.14f);
                                                                                 d.f(3.14f);                                        <-- 1
                                                                               }
                                                                               int main()
                                                                               {
                                                                                 Derived d;
                                                                                 sample(d, d);
                                                                               }
                                                                               

                                                                              (1) This is not hidden (good!)

                                                                              The output of this program demonstrates that the behavior depends on the type of the object, not the type of the reference:

                                                                               Derived::f(int)
                                                                               Derived::f(int)
                                                                               Base::f(float)
                                                                               Base::f(float)
                                                                               

                                                                              This guideline applies only to public inheritance; hiding base class member functions is fine for private or protected inheritance (see FAQ 37.01).

                                                                                FAQ 29.05 Can virtual functions be overloaded?

                                                                                Yes, but it's often easier to use nonvirtual overloads that call nonoverloaded virtuals.

                                                                                As was discussed in FAQ 29.02, when virtual member functions are overloaded, the hiding rule forces derived classes to do a bit more work than necessary. In these situations, it is often easier if the overloads are nonvirtuals that call virtuals that aren't overloaded. These nonoverloaded virtuals are normally protected:.

                                                                                The following code shows how to apply this guideline to the situation in the previous FAQ where Base::f(int) and Base::f(float) are overloaded virtuals. These functions are now nonvirtuals that call nonoverloaded virtuals f_i(int) and f_f(float). (Don't redefine nonvirtual member functions; see FAQ 29.02.)

                                                                                 #include <iostream>
                                                                                 using namespace std;
                                                                                 
                                                                                 class Base {
                                                                                 public:
                                                                                   virtual ~Base() throw();
                                                                                   void f(int x)   throw();
                                                                                   void f(float x) throw();
                                                                                 protected:
                                                                                   virtual void f_i(int x)   throw();
                                                                                   virtual void f_f(float x) throw();
                                                                                 };
                                                                                 inline void Base::f(int x) throw()                   <-- 1
                                                                                   { f_i(x); }
                                                                                 inline void Base::f(float x) throw()
                                                                                   { f_f(x); }
                                                                                 void Base::f_i(int x) throw()                        <-- 2
                                                                                   { cout << "Base::f(int)\n"; }
                                                                                 void Base::f_f(float x) throw()
                                                                                   { cout << "Base::f(float)\n"; }
                                                                                 
                                                                                 Base::~Base() throw()
                                                                                   { }
                                                                                 

                                                                                (1) Overloaded nonvirtuals

                                                                                (2) Nonoverloaded virtuals

                                                                                In class Derived, the behavior of f(int) is changed by overriding Base::f_i(int); redefining f(int) itself would be wrong, since a redefinition would hide Base::f(float).

                                                                                 class Derived : public Base {
                                                                                 public:
                                                                                                                                      <-- 1
                                                                                 protected:
                                                                                   virtual void f_i(int x) throw();                   <-- 2
                                                                                 };
                                                                                 
                                                                                 void Derived::f_i(int x) throw()
                                                                                   { cout << "Derived::f(int)\n"; }
                                                                                 

                                                                                (1) Derived classes never redefine f(int)

                                                                                (2) Derived classes may override f_i(int)

                                                                                Now when member function f(int) is invoked on a Derived object, it expands inline as the code of Base::f(int), which calls protected member function f_i(int), and since f_i(int) is virtual, it resolves to the correct member function using dynamic binding (see FAQ 2.24). The message here is that both f(int) and f(float) work correctly on both a Derived& and on a Base&:

                                                                                 void sample(Base& b, Derived& d)
                                                                                 {
                                                                                   b.f(42);
                                                                                   d.f(42);
                                                                                 
                                                                                   b.f(3.14f);
                                                                                   d.f(3.14f);                                        <-- 1
                                                                                 }
                                                                                 
                                                                                 int main()
                                                                                 {
                                                                                   Derived d;
                                                                                   sample(d, d);
                                                                                 }
                                                                                 

                                                                                (1) This is not hidden (good!)

                                                                                The output of this program demonstrates that the behavior depends on the type of the object, not the type of the reference:

                                                                                 Derived::f(int)
                                                                                 Derived::f(int)
                                                                                 Base::f(float)
                                                                                 Base::f(float)
                                                                                 

                                                                                This approach is more scalable than the approach presented in the earlier FAQ. It is scalable in two ways, with respect to the code and with respect to the people. With respect to the code, the root of the inheritance hierarchy has a few extra lines of code, but none of the (potentially many) derived classes need have any extra code to handle the hiding rule. This is a good trade-off since an inheritance hierarchy often has many derived classes. With respect to the people, the developer of the root class of the inheritance hierarchy needs to understand the hiding rule, but all the writers of all the derived classes can remain relatively ignorant of itthey need to know only that they are to override the virtual member functions rather than the nonvirtual member functions. This is a good trade-off because the developers who build the root classes in the inheritance hierarchies are normally more sophisticated than the developers who build the derived classes.

                                                                                Note that this approach does not imply any performance overhead, since the overloaded public: member functions are normally inline nonvirtuals.

                                                                                As before, this guideline applies only to public inheritance; hiding base class member functions is fine for private or protected inheritance (see FAQ 37.01).

                                                                                  Оставьте свой комментарий !

                                                                                  Ваше имя:
                                                                                  Комментарий:
                                                                                  Оба поля являются обязательными

                                                                                   Автор  Комментарий к данной статье