Effective C++读书笔记 Part2

Effective_CPP_Note2

Effective C++ Notes Part II

Part II. Constructors, Destructors and Assignment Operators

5. Know what functions C++ silently writes and calls. (C++ 03)

  • If any of default constructor, default destructor, copy constructor or assignment operator is not defined manually, compiler will provide a default public inline version of it. Particularlly, the compiler provided destructor is a non-virtual version unless its base class contains a virtual destructor and both copy constructor and assignment operator simply copy every non-static member variables. But if compiler generated code is illegal or nonsense,the compiler will refuse to generate such codes.

    A class is defined as follow:

    template<class T>
    class NamedObject
    {
    public:
        NamedObject(std::string& name, const T& value);
    private:
        std::string& nameValue;
        const T objectValue;
    };
    

    Then we use it like this:

    std::string newDog("A");
    std::string oldDog("B");
    NamedObject<int> p(newDog, 2);
    NamedObject<int> s(oldDog, 36);
    p = s;
    

    we know that reference can't be re-directed and const members can't be changed. So the compiler will refuse to compile the line 'p=s'

6. Explicity disallow the use of compiler-generated functions you do not want

  • In some cases a class should be disallowd to be copied, but if you don't provide a copy constructor nor a assignment operator for it, the compiler would do, thus the class would become copyable. We can disable such operations via define copy constructor and assignment operator as private but this is not absolutely safe since member functions and friend class/functions still can call them, otherwise we will throw the problem to linker via undefined copy operations. But, a compile-time error is far more better than link-time error, a base class with private copy operations will do that.

    class NonCopyable
    {
        public:
            NonCopyable() {}
            virtual ~NonCopyable() {}
        private:
            NonCopyable(const NonCopyable&);
            NonCopyable& operator=(const NonCopyable&);
    };
    
    class MyClass : private NonCopyable
    {
    ...
    };
    

    Let our class inherit this NonCopyable class, copy operations in derived class will try to call such operations in base class those are declared private which the compiler won't let happen and we got a compile-time error, exactly what we want.

7. Declare destructors virtual in polymorphic base classes

  • There is a implicit trap in polymorphic structures, if we don't declare the destructor of base class virtual, something unexpected may happen.

    class A
    {
        A() : a_(new int(1)) {}
        ~A() { delete a_; }
        int *a_;
    };
    
    class B : public A
    {
        B() : A(), b_(new int(2)) {}
        ~B() { delete b_; }
        int *b_;
    };
    

    if we use these class like this

    B *pb = new B();
    A *pa = pb;
    delete pa;
    

    It is OK in syntax because B is inherited from A, but the delete statement actually called ~A() other than ~B(), thus b_ in class B is not released which leads to the memory leak. To deal with such situation, we simply need to declare ~A() as virtual.

    • If a class possesses any virtual, whose destructor should be declared virtual since a virtual class must be designed to act as a base class, a base class need its destructor to be virtual in order to avoid partly released resources and potential resource leak.

    • A class with no virutal method still may be used as base class, whenever a class is used as base class in polymorphic structure, it needs a virtual desctructor. Inherit a class with non-virtual destructor, such as std::string may cause unspecified behaviors.

    • Declare destructor of a class which won't be derived virtual is not wise, if any method of a class is declared virtual, compiler will provide a vtable pointer to look up the vtable for the class which need sizeof(void*) bytes of memory. This will result in the loss of compatitance with C since the instance won't have a plain memory.

  • It is convenient to declare a abstruct class by declaring a pure virtual method, Thus the class is designed to be used as base class so it need a virtual destructor. But in a polymorphic structure, the desctructor of the derived class is called and then calls the base class destructor. A pure virtual destructor here may cause link error so we need to provide a definition for it.

8. Prevent exceptions from leaving destructors

  • In containers or arrays storing user-defined types, an uncatched exception in the descturctor of such type may be throwed multiple times and results in undefined behaviors.

  • If destructor of a class need to call a function which may throw exceptions, RAII classes as an example, if the function runs well then everything is on the track, but if the function fails, it may throw a exception.

    class DBConnection
    {
        public:
            static DBConnection create();
            void close();
    };
    
    //We will adopt RAII to manage the DBConnection
    
    class DBConn
    {
        public:
            ...
            ~DBConn() { db.close(); }
        private:
            DBConnection db;
    };
    

    To avoid such problem, we can just kill the process itself via abort()

    DBConn::~DBConn()
    {
        try { db.close(); }
        catch(...)
        {
            Log the exception
            std::abort();
        }
    }
    

    It's reasonable to kill a process if the control flow can't be continued if any exception is throw in destruction. This may prevent the exception from throwing to the outer block which may cause undefined behaviors.

    Or we can just Log the exception and let the process continue

    DBConn::~DBConn
    {
        try { db.close(); }
        catch(...)
        {
            Log the exception
        }
    }
    

    It is not wise to ignore an exception in most circumstances because this may hide the information about a serious error, but if an error is not serious and ignorance of which won't trigger any serious effect then it is applicable to ignore such exceptions.

    Both approaches above are not perfect for not providing the users a chance to handle the exceptions, in order to do so, we can redesign the DBConn class as below.

    class DBConn {
        public:
            ...
            void close() { db.close(); }
            ~DBConn()
            {
                if(!closed)
                {
                    try { db.close(); }
                    catch(...) {
                        Log the exception then abort the process or just ignore it
                    }
                }
            }
        private:
            DBConnection db;
            bool closed;
    };
    

    The close() method provides our user an approch to handle the exception in DBConnection::close() and we still provide a failsafe in destructor. This design won't add complexity to the interface. An exception in destruction is very dangerous as we can hardly handle the exception correct in the destructor with no understanding of outside business logic.

9. Never call virtual functions during construction or destruction

  • Consider the code below

    class Base
    {
        public:
            Base();
            virtual foo() const = 0;
    };
    
    Base::Base()
    {
        foo();
    }
    
    class Derived : public Base
    {
        public:
            virtual foo() const;
    }
    
    Derived instance;
    

    As we know, when we instancize a derived object, Base() is called first followed by Derived(). But before ~Derived() is called, the type of the constructing object is Base instead of Derived, thus the Base constructor would call the Base::foo() instead of Derived::foo() which is obviously not what we want. In this example foo() is pure so we would get a link error, but if the base class provide a default definition of foo(), we may have a compile time warning if we are lucky, otherwise we will spend lots of time on debugging.

  • It is the same when it comes to destructor, once ~Derived() is called, the derived members become undefined, When ~Base() is called, the type of such object is considered Base.

  • There are other hidden traps when the Base class contains several overloaded constructor, it is very common to provide an initializer to avoid duplicating codes and the initializer calls a virtual function.

    class Base
    {
        public:
            Base() { init(); }
            virtual void foo() const = 0;
        private:
            void init() { foo(); }
    };
    

    This mistake is hidden deeper than previous and is hard to notice it. To avoid been trapped, we must ensure that no virtual functions are called in both constructor and destructor explicitly.

  • There are other approaches, one among them is modify the virtual function to non-virtual and pass the information needed to the base constructor.

    class Base
    {
        public:
            explicit Base(const std::string& logInfo)
            {   Log(logInfo); }
            void Log(const std::string& logInfo);
    };
    
    class Derived : public Base
    {
        public:
            Derived(parameter)
                : Base(makeString(parameter))
            { ... }
    }
    

    As we explained above that virtual functions can't be pass down along the inheritance link but we can pass the required constructing info upside to base constructor.

10. Have assignment operators return a reference to *this

  • For built-in types and STL types we can write assignment in a link like this

    int x, y, z;
    x = y = z = 1;
    

    if we disassamble this expression, it will be

    x.operator= ( y.operator= (z) )
    

    To achieve this in custom types, we need define the opertor= like this

    class Widget
    {
        public:
            Widget& operator=(const Widget& rhs)
            {
                ... copy the members ...
                return *this;
            }
    }
    

    This rule is not applicable to operator= but also all assignment operators such as +=, -=, *=, etc. Even the parameter doesn't share the same type with return value, the rule should still be adopted.

    Widget& Widget::operator+=(int rhs)
    {
        //do the copy
        return *this;
    }
    

    This rule is not forced to be obeyed but it's adopted by all built-in types and STL types, even just in consideration of STL compatibility we should obey it.

  • In my opinion assignment operator should be defined to return a const reference to *this in order to avoid such operations

    object a,b,c;
    (a=b)=c;
    

11. Handle assignment to self in opertor=

  • Self Assignment happens if an object calls operator= with itself as parameter, aliases such as references and pointers offten cause self assignments. Assignment of a class may result in catastrophy if self assignment is not correctly handled.

    class Bitmap{...}
    class Widget
    {
        ...
        Bitmap *bp;
    };
    
    Widget& Widget::operator=(const Widget& rhs)
    {
        delete bp;
        bp = new Bitmap(*rhs.bp);
        return *this;
    }
    

    If we call operator of an Widget object with itself as parameter, the Bitmap object hold by the Widget is freed, this is absolutely not what we want. An equality check just after entering operator= may solve the problem

    Widget& Widget::operator=(const Widget& rhs)
    {
        if (this == rhs) return *this;
        delete bp;
        bp = new Bitmap(*rhs.bp);
        return *this;
    }
    

    This approach is self assignment safe but still not exception safe, if an exception throw in construction of new Bitmap, we still get an empty Bitmap pointer which we can't read them nor delete them. A exception safe assignment operator is offten also self assignment safe and all we need to do is to save a copy of bp before we successfully copied the exact resource.

    Widget& Widget::operator=(const Widget& rhs)
    {
        Bitmap* pOrig = bp;
        bp = new Bitmap(*rhs.bp);
        delete pOrig;
        return *this;
    }
    

    With efficiency concerns on high self assignment frequency, we may add the identity test back to the beginning of operator= which may cause a bigger compiled target and tiny or negtive uptick on performance depends on self assignment frequency.

    Another approach to implement a self assignment and exception safe operator= is called copy and swap, first we get a copy of rhs and then we swap it with *this.

    Widget& Widget::operator=(const Widget& rhs)
    {
        Widget tmp(rhs);
        swap(tmp);
        return *this;
    }
    //An simpler but not very clear approach
    Widget& Widget::operator=(Widget rhs) //The parameter is passed by value(copy)
    {
        swap(rhs);
        return *this;
    }
    

12. Copy all parts of an object

  • An implementation of copy operation which copies only some part of the object is offten considered to be OK by most compilers, which means you won't get an error nor a warning on it.

    class Date {...};
    class Customer
    {
        public:
            Customer(const Customer& rhs);
            Customer& operator=(const Customers& rhs);
            ...
        private:
        I   std::string name_;
    };
    
    Customer::Customer(const Customer& rhs) : name_(rhs.name_) {}
    Customer& Customer::operator=(const Customer& rhs)
    {
        //deal with self assignments
        name_ = rhs.name_;
        return *this;
    }
    

    It seems that everything is OK here and they are OK indeed until class Customer's private members become

    class Customer
    {
        //Same with Customer above
        private:
            std::string name_;
            Date lastTransaction_;
    };
    

    Now the copy functions just copy half of a Customer's members and the compiler doesn't take this kind of partial copy as an error. It is obvious that if we change a class definition, we need to revise the copy functions for it to match the revised class.

  • When it comes to a inheritance structure, traps are hiden more deeper.

    class Vip : public Customer
    {
        public:
            Vip(const Vip& rhs);
            Vip& operator=(const Vip& rhs);
        private:
            int level_;
    };
    
    Vip::Vip(const Vip& rhs) : level_(rhs.level_) {}
    Vip& Vip::operator=(const Vip& rhs)
    {
        level_ = rhs.level_;
        return *this;
    }
    

    An Vip instance will contains two parts, one is level_ and the other is a copy of a Customer object. It's not wisdom to count on the compiler to take care of the Customer object just because it is not explicit showed on screen. Compiler won't take the job nor it will warns you the existance of the objectm, so we must do this carefully in avoidance of some unexpected behaviors. And you can't get private members from the base class object, so a Base copy constructor is needed.

    Vip::Vip(const Vip& rhs)
    : Customer(rhs),
      level_(rhs.level_)
    {
    }
    
    Vip& Vip::operator=(const Vip& rhs)
    {
        Customer::operator=(rhs);
        level_ = rhs.level_;
        return *this;
    }
    
  • The copy constructor and assignment operator offten have similar implementations, but it's irreasonable to implement one by calling another

    • Let copy assignment operator call copy constructor is meaningless. WHat would happen if we try to construct an existing object?

    • Let copy construct call copy assignment operator is also meaningless. The copy constructor is designed to construct a new object which contains the entry of copy assignment operator. Calling the copy assignment operator during construction, which means the target object is not really exist, is meaningless.

    • To avoid code duplication, one appropriate approach is to provide a new private member function called init()

Conclusion:

5.1 Compilers may implicitly generate a class's default constructor, copy constructor, copy assignment operator and destructor

6.1 To disallow functionality automatically provided by compilers, declare the corresponding member functions private and give no implementations. Using a base class like Uncopyable is one way to do this.

7.1 Polymorphic base classes should declare virtual destructors. If a class has any virtual functions, it should have a virtual destructor.

7.2 Classes not designed to be base classes or not designed to be used polymorphically should not declare virtual destructors

8.1 Destructors should never emit exception. If functions called in a destructor may throw, the destructor should catch ant exceptions, then swallow them or terminate the program.

8.2 If class clients need to be able to react to exceptions thrown during an operation, the class should provide a regular(i.e., non-destructor) function that performs the operation.

9.1 Don't call virtual functions during construction or destruction, beacuse such calls will never go to a more derived class than that of the currently executing constructor or destructor.

10.1 Have assignment operators return a reference to *this.

11.1 Make sure operator= is well-behaved when an object is assigned to itself. Techniques include comparing addresses of source and target object, careful statement ordering, and copy-and-swap.

11.2 Make sure that any function operating on more than one object behaves correctly if two or more of the objects are the same.

12.1 Copying functions should be sure to copy all of an object's data members and all of its base class parts.

12.2 Don't try to implement one of the copying functions in terms of the other. Instead, put common functionality in a third function that both call.

原文地址:https://www.cnblogs.com/current/p/4228284.html