C++ Chapter 5. Implementations

1. Postpone variable definitions as long as possible. It increases program clarity and improves program efficiency. Not only should you postpone a variable's definition until right before you have to use the variable, you should also try to postpone the definition until you have initialization arguments for it. By doing so, you avoid constructing and destructing unneeded objects, and you avoid unnecessary default constructions.

// Approach A: define outside loop   // Approach B: define inside loop



Widget w;

for (int i = 0; i < n; ++i){         for (int i = 0; i < n; ++i) {

  w = some value dependent on i;       Widget w(some value dependent on i);

  ...                                  ...

}                                    }


Here I've switched from an object of type string to an object of typeWidget to avoid any preconceptions about the cost of performing a construction, destruction, or assignment for the object.

In terms of Widget operations, the costs of these two approaches are as follows:

  • Approach A: 1 constructor + 1 destructor +n assignments.

  • Approach B: n constructors +n destructors.

For classes where an assignment costs less than a constructor-destructor pair, Approach A is generally more efficient. This is especially the case asn gets large. Otherwise, Approach B is probably better. Furthermore, Approach A makes the namew visible in a larger scope (the one containing the loop) than Approach B, something that's contrary to program comprehensibility and maintainability. As a result, unless you know that (1) assignment is less expensive than a constructor-destructor pair and (2) you're dealing with a performance-sensitive part of your code, you should default to using Approach B.


2.

Let's begin with a review of casting syntax, because there are usually three different ways to write the same cast. C-style casts look like this:

(T) expression                      // cast expression to be of type T


Function-style casts use this syntax:

T(expression)                       // cast expression to be of type T
  • const_cast is typically used to cast away the constness of objects. It is the only C++-style cast that can do this.

  • dynamic_cast is primarily used to perform "safe downcasting," i.e., to determine whether an object is of a particular type in an inheritance hierarchy. It is the only cast that cannot be performed using the old-style syntax. It is also the only cast that may have a significant runtime cost. (many implementations ofdynamic_cast can be quite slow)

    It is used when you only have a base class pointer to derived class object but you want to access some function in derived object(non- virtual function).

     The dynamic_cast operator ensures that if you convert a pointer of class A to a pointer of a class B, the object that points to belongs to class B or a class derived from B.(otherwise the cast will fail)

    #include <iostream>
    using namespace std;
    
    struct A {
      virtual void f() { cout << "Class A" << endl; }
    };
    
    struct B : A {
      virtual void f() { cout << "Class B" << endl; }
    };
    
    struct C : A {
      virtual void f() { cout << "Class C" << endl; }
    };
    
    void f(A* arg) {
      B* bp = dynamic_cast<B*>(arg);
      C* cp = dynamic_cast<C*>(arg);
    
      if (bp)
        bp->f();
      else if (cp)
        cp->f();
      else
        arg->f();
    };
    
    int main() {
      A aobj;
      C cobj;
      A* ap = &cobj;
      A* ap2 = &aobj;
      f(ap);
      f(ap2);
    }
    

    The following is the output of the above example:

    Class C
    Class A

  • reinterpret_cast is intended for low-level casts that yield implementation-dependent (i.e., unportable) results, e.g., casting a pointer to anint. Such casts should be rare outside low-level code. I use it only once in this book, and that's only when discussing how you might write a debugging allocator for raw memory (seeItem 50).

  • static_cast can be used to force implicit conversions (e.g., non-const object toconst object (as inItem 3),int todouble, etc.). It can also be used to perform the reverse of many such conversions (e.g.,void* pointers to typed pointers, pointer-to-base to pointer-to-derived), though it cannot cast fromconst to non-const objects. (Onlyconst_cast can do that.)

    The need for dynamic_cast generally arises because you want to perform derived class operations on what you believe to be a derived class object, but you have only a pointer- or reference-to-base through which to manipulate the object. There are two general ways to avoid this problem.

class Widget {

public:

  explicit Widget(int size);

  ...

};



void doSomeWork(const Widget& w);



doSomeWork(Widget(15));                    // create Widget from int

                                           // with function-style cast it is not calling constructor 



doSomeWork(static_cast<Widget>(15));       // create Widget from int

                                           // with C++-style cast


Somehow, deliberate object creation doesn't "feel" like a cast, so I'd probably use the function-style cast instead of thestatic_cast in this case. Then again, code that leads to a core dump usually feels pretty reasonable when you write it, so perhaps you'd best ignore feelings and use new-style casts all the time.



3.This last example demonstrates that a single object (e.g., an object of typeDerived) might have more than one address (e.g., its address when pointed to by aBase* pointer and its address when pointed to by aDerived* pointer). 


4. References, pointers, and iterators are all handles (ways to get at other objects), and returning a handle to an object's internals always runs the risk of compromising an object's encapsulation.

class Window {                                // base class

public:

  virtual void onResize() { ... }             // base onResize impl

  ...

};



class SpecialWindow: public Window {          // derived class

public:

  virtual void onResize() {                   // derived onResize impl;

    static_cast<Window>(*this).onResize();    // cast *this to Window,

                                              // then call its onResize;

                                              // this doesn't work!



    ...                                       // do SpecialWindow-

  }                                           // specific stuff



  ...



};
The above code doesn't call Window::onResize on the current object and then perform the SpecialWindow-specific actions on that object — it calls Window::onResize on a copy of the base class part of the current object before performing SpecialWindow-specific actions on the current object. 


5. 

When an exception is thrown, exception-safe functions:

  • Leak no resources. The code above fails this test, because if the "new Image(imgSrc)" expression yields an exception, the call tounlock never gets executed, and the mutex is held forever.

  • Don't allow data structures to become corrupted. If "new Image(imgSrc)" throws,bgImage is left pointing to a deleted object. In addition,imageChanges has been incremented, even though it's not true that a new image has been installed. (On the other hand, the old image has definitely been eliminated, so I suppose you could argue that the image has been "changed.")

    Exception-safe functions offer one of three guarantees:

    • Functions offering the basic guarantee promise that if an exception is thrown, everything in the program remains in a valid state. No objects or data structures become corrupted, and all objects are in an internally consistent state (e.g., all class invariants are satisfied). However, the exact state of the program may not be predictable. For example, we could writechangeBackground so that if an exception were thrown, thePrettyMenu object might continue to have the old background image, or it might have some default background image, but clients wouldn't be able to predict which. (To find out, they'd presumably have to call some member function that would tell them what the current background image was.)

    • Functions offering the strong guarantee promise that if an exception is thrown, the state of the program is unchanged. Calls to such functions areatomic in the sense that if they succeed, they succeed completely, and if they fail, the program state is as if they'd never been called.

      Working with functions offering the strong guarantee is easier than working with functions offering only the basic guarantee, because after calling a function offering the strong guarantee, there are only two possible program states: as expected following successful execution of the function, or the state that existed at the time the function was called. In contrast, if a call to a function offering only the basic guarantee yields an exception, the program could be inany valid state.

    • Functions offering the nothrow guarantee promise never to throw exceptions, because they always do what they promise to do. All operations on built-in types (e.g.,ints, pointers, etc.) are nothrow (i.e., offer the nothrow guarantee). This is a critical building block of exception-safe code.

    • int doSomething() throw();          // note empty exception spec.
      
      

      This doesn't say that doSomething will never throw an exception; it says thatifdoSomething tHRows an exception, it's a serious error, and theunexpected function should be called.[1] In fact,doSomething may not offer any exception guarantee at all. The declaration of a function (including its exception specification, if it has one) doesn't tell you whether a function is correct or portable or efficient, and it doesn't tell you which, if any, exception safety guarantee it offers, either. All those characteristics are determined by the function's implementation, not its declaration.

    There is a general design strategy that typically leads to the strong guarantee, and it's important to be familiar with it. The strategy is known as"copy and swap." In principle, it's very simple. Make a copy of the object you want to modify, then make all needed changes to the copy. If any of the modifying operations throws an exception, the original object remains unchanged. After all the changes have been successfully completed, swap the modified object with the original in a non-throwing operation.

    struct PMImpl {                               // PMImpl = "PrettyMenu
    
      std::tr1::shared_ptr<Image> bgImage;        // Impl."; see below for
    
      int imageChanges;                           // why it's a struct
    
    };
    
    
    
    class PrettyMenu {
    
      ...
    
    
    
    private:
    
      Mutex mutex;
    
      std::tr1::shared_ptr<PMImpl> pImpl;
    
    };
    
    
    
    void PrettyMenu::changeBackground(std::istream& imgSrc)
    
    {
    
      using std::swap;                            // see Item 25
    
    
    
      Lock ml(&mutex);                            // acquire the mutex
    
    
    
      std::tr1::shared_ptr<PMImpl>                // copy obj. data
    
        pNew(new PMImpl(*pImpl));
    
    
    
      pNew->bgImage.reset(new Image(imgSrc));     // modify the copy
    
      ++pNew->imageChanges;
    
    
    
      swap(pImpl, pNew);                          // swap the new
    
                                                  // data into place
    
    
    
    }                                             // release the mutex
    

    6.

    If an inline function body is very short, the code generated for the function body may be smaller than the code generated for a function call. If that is the case, inlining the function may actually lead to smaller object code and a higher instruction cache hit rate!

    Bear in mind that inline is a request to compilers, not a command. The request can be given implicitly or explicitly. The implicit way is to define a function inside a class definition:

    Inline functions must typically be in header files, because most build environments do inlining during compilation. In order to replace a function call with the body of the called function, compilers must know what the function looks like

    Templates are typically in header files, because compilers need to know what a template looks like in order to instantiate it when it's used.

    Before we do that, let's finish the observation that inline is a request that compilers may ignore. Most compilers refuse to inline functions they deem too complicated (e.g., those that contain loops or are recursive), and all but the most trivial calls to virtual functions defy inlining. This latter observation shouldn't be a surprise.virtual means "wait until runtime to figure out which function to call," andinline means "before execution, replace the call site with the called function." If compilers don't know which function will be called, you can hardly blame them for refusing to inline the function's body.


    7.

    Sometimes compilers generate a function body for an inline function even when they are perfectly willing to inline the function. For example, if your program takes the address of an inline function, compilers must typically generate an outlined function body for it. How can they come up with a pointer to a function that doesn't exist? Coupled with the fact that compilers typically don't perform inlining across calls through function pointers, this means that calls to an inline function may or may not be inlined, depending on how the calls are made:

    inline void f() {...}      // assume compilers are willing to inline calls to f
    
    
    
    void (*pf)() = f;          // pf points to f
    
    
    
    ...
    
    
    
    f();                      // this call will be inlined, because it's a "normal" call
    
    pf();                     // this call probably won't be, because it's through
    
                              // a function pointer
    
    
    Don't use inline key word to constructor and destructor.
    Library designers must evaluate the impact of declaring functions inline, because it's impossible to provide binary upgrades to the client visible inline functions in a library. In other words, if f is an inline function in a library, clients of the library compile the body of f into their applications. If a library implementer later decides to change f, all clients who've used f must recompile. This is often undesirable. On the other hand, if f is a non-inline function, a modification to f requires only that clients relink. This is a substantially less onerous burden than recompiling and, if the library containing the function is dynamically linked, one that may be absorbed in a way that's completely transparent to clients.
    
    8. 
    #include <string>                      // standard library components
    
                                           // shouldn't be forward-declared
    
    
    
    #include <memory>                      // for tr1::shared_ptr; see below
    
    
    
    class PersonImpl;                      // forward decl of Person impl. class
    
    class Date;                            // forward decls of classes used in
    
    
    
    class Address;                         // Person interface
    
    class Person {
    
    public:
    
     Person(const std::string& name, const Date& birthday,
    
            const Address& addr);
    
     std::string name() const;
    
     std::string birthDate() const;
    
     std::string address() const;
    
     ...
    
    
    
    private:                                   // ptr to implementation;
    
      std::tr1::shared_ptr<PersonImpl> pImpl;  // see Item 13 for info on
    
    };                                         // std::tr1::shared_ptr
    
    
    • Avoid using objects when object references and pointers will do(As in the example above). You may define references and pointers to a type with only a declaration for the type. Defining objects of a type necessitates the presence of the type's definition.

      Because by doing so, when we create an instance of the class, we don't need to know the size of the object we point to. If we just use object instead of reference, we can't create the instance by just knowing the declare.

    • Depend on class declarations instead of class definitions whenever you can. Note that you never need a class definition to declare a function using that class, not even if the function passes or returns the class type by value:

      class Date;                        // class declaration
      
      
      
      Date today();                      // fine — no definition
      
      void clearAppointments(Date d);    // of Date is needed
      
      
            
            
      • Provide separate header files for declarations and definitions. In order to facilitate adherence to the above guidelines, header files need to come in pairs: one for declarations, the other for definitions. These files must be kept consistent, of course. If a declaration is changed in one place, it must be changed in both. As a result, library clients should always #include a declaration file instead of forward-declaring something themselves, and library authors should provide both header files.(include "class Data" in a seperate header file)

        The general idea behind minimizing compilation dependencies is to depend on declarations instead of definitions. Two approaches based on this idea are Handle classes and Interface classes.

        1. Handle Class:

          #include "Person.h"       // we're implementing the Person class,
        
                                     // so we must #include its class definition
        
        
        
        #include "PersonImpl.h"      // we must also #include PersonImpl's class
        
                                     // definition, otherwise we couldn't call
        
                                     // its member functions; note that 
        
                                     // PersonImpl has exactly the same
        
                                     // member functions as Person — their
        
                                     // interfaces are identical
        
        
        
        Person::Person(const std::string& name, const Date& birthday,
        
                       const Address& addr)
        
        : pImpl(new PersonImpl(name, birthday, addr))
        
        {}
        
        
        
        std::string Person::name() const
        
        {
        
          return pImpl->name();
        
        }
        
        
        2. Interface Class:

        class Person {
        
        public:
        
          virtual ~Person();
        
        
        
          virtual std::string name() const = 0;
        
          virtual std::string birthDate() const = 0;
        
          virtual std::string address() const = 0;
        
          ...
        
        };
        
        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值