weidagang2046的專欄

          物格而后知致
          隨筆 - 8, 文章 - 409, 評論 - 101, 引用 - 0
          數據加載中……

          C++ constructor trivia

          We had a head-scratcher bug the other day. Our system is written in C++, and has a custom reference counting system for managing memory (see ). We were seeing an assertion that we had thought was long debugged and solid. Here's what we found. (Warning: lots of arcane C++ stuff ahead.)

          Our reference counting implementation looks kind of like this:

           1/**
           2 * Reference-counted object.  When its reference count goes to
           3 * zero, it destroys itself.
           4 */
           5class CRefCountedObject
           6{
           7public:
           8    CRefCountedObject()
           9    {
          10        m_nRefCount = 0;
          11    }
          12
          13
              virtual ~CRefCountedObject()
          14    {
          15        
          // CRefCountedObject's can be created on the stack,
          16        
          // where they will be destroyed by falling out of scope.
          17        
          // This assert checks the implicit assumption that no one
          18        
          // is still interested in the object when it is destroyed.
          19        ASSERT(m_nRefCount == 0);
          20    }
          21
          22
              /** Declare your interest in this object: ups the refcount.
          23     */
          24    void Retain()
          25    {
          26        m_nRefCount++;
          27    }
          28
          29
              /** Declare your lack of interest in this object: if you were
          30     *  the last interested party, the object is destroyed.
          31     */
          32    void Release()
          33    {
          34        m_nRefCount--;
          35        if (m_nRefCount== 0) {
          36            delete this;
          37        }
          38    }
          39
          40
          private:
          41    int m_nRefCount;
          42};
          43
          44
          /**
          45 * Smart pointer, templated to a particular object class.
          46 */
          47template <class TObject>
          48class CPtr
          49{
          50public:
          51    
          // .. lots of stuff omitted ..
          52
          53
              /** Assignment operator.  Manage the refcounts on the old and
          54     *  new objects.
          55     */
          56    TObject *operator=(TObject *p)
          57    {
          58        if (m_p != p) {
          59            if (p != NULL) {
          60                p->Retain();
          61            }
          62            if (m_p != NULL) {
          63                m_p->Release();
          64            }
          65            m_p = p;
          66        }
          67        return m_p;
          68    }
          69
          70
              
          //.. lots of stuff omitted ..
          71};

          (Note: this is a simplified version of our actual code, and some of the simplifications mean that it will not work properly, but not in ways that affect the narrative here. For example, don't try this code with multiple threads, or with multiple inheritance. I'm taking some expository license. Don't bug me about it!)

          The assert that fired is on line 19: it's designed to protect against allocating reference counted objects on the stack. The problem with a stack-allocated refcounted object is that the object will be destroyed when it falls out of scope, regardless of the reference count. For our reference counting to work right, the only thing that should destroy an object is the delete on line 36. With a stack allocated object, the compiler destroys the object for us, so smart pointers can exist which point to freed memory:

          100{
          101    CPtr<CRefCountedObject> pObj;
          102
          103
              
          //.. blah blah ..
          104
          105
              if (blah blah) {
          106        CRefCountedObject stackobj;
          107        pObj = stackobj;
          108    }
          109
          110
              
          // pObj is invalid at this point.
          111}

          The object stackobj is created at line 106, and then destroyed at line 108 (when it falls out of scope), but pObj still has a pointer to it. When stackobj is destroyed, its reference count is 1, so the assert in the CRefCountedObject destructor will fire. All is well.

          So when that assertion fired the other day, we thought we understood the problem. Just look up the stack, find the stack allocated refcounted object, and change it to a heap allocated object. But when we looked into it, there was no stack allocated object. So who was destroying the refcounted object before its time? How does a heap allocated object get destroyed other than using delete on it?

          Digging deeper, we rediscovered a C++ detail that we had forgotten. Turns out we had some code that boiled down to this:

          200class CBadObject: public CRefCountedObject
          201{
          202    
          //.. blah blah ..
          203};
          204
          205
          CPtr<CBadObject> pGlobalBad;
          206
          207
          CBadObject::CBadObject()
          208{
          209    pGlobalBad = this;
          210
          211
              DoSomethingWhichThrowsAnException();
          212}

          This object's constructor assigned itself to a smart pointer outside itself, then threw an exception. C++ semantics state that if a constructor of a heap allocated object throws an exception, that the object's destructor is called. Since CBadObject derives from CRefCountedObject, the CRefCountedObject destructor is called. It checks the reference count, sees that it is not zero (it's one, because pGlobalBad has called Retain, but hasn't called Release), and fires the assertion.

          Updated May 2005: Actually, the correct statement of C++ semantics is that if a constructor throws an exception, a destructor is called for all the base classes that had been successfully constructed. In this case, the CBadObject destructor is not called (because the CBadObject constructor didn't finish), but the CRefCountedObject destructor is called, because its constructor completed. It's the CRefCountedObject destructor which causes the trouble here. More about this at More C++ constructor trivia.

          So the lesson is: try not to throw exceptions from constructors, or at least understand that they will destroy the object for you.

          Updated May 2005: Again, this is not correct. The real lesson is that throwing exceptions from constructors is perfectly legal, and C++ is precisely engineered to specify exactly what will happen. But you may be surprised by what happens. In particular, you may have objects in "impossible" states.

          It just goes to show you:

          You learn something new every day, no matter how hard you try.

          By the way: there were two things we did to fix the problem. Do you know what they were? The first was to move most of the code out of the constructor, so that hard stuff happens in regular methods. The second was to add to the comment in CRefCountedObject's destructor:

          13    virtual ~CRefCountedObject()
          14    {
          15        
          // CRefCountedObject's can be created on the stack,
          16        
          // where they will be destroyed by falling out of scope.
          17        
          // This assert checks the implicit assumption that no one
          18        
          // is still interested in the object when it is destroyed.
          19        
          // The other way this can happen is if a constructor
          20        
          // fails, but it has already assigned itself to a CPtr
          21        
          // someplace.  When a C++ constructor fails, the compiler
          22        
          // automatically destroys the object.
          23        ASSERT(m_nRefCount == 0);
          24    }

          That way, future generations might be spared the head-scratching.

          from: http://www.nedbatchelder.com/blog/20041202T070735.html

          posted on 2005-10-23 22:55 weidagang2046 閱讀(646) 評論(0)  編輯  收藏 所屬分類: C/C++

          主站蜘蛛池模板: 泾阳县| 三台县| 台北县| 临安市| 福州市| 阜宁县| 通海县| 读书| 义乌市| 伽师县| 嘉荫县| 隆回县| 天水市| 巨鹿县| 龙江县| 肥乡县| 外汇| 民丰县| 东城区| 西吉县| 丹东市| 新晃| 白水县| 太康县| 太仓市| 丹凤县| 栾川县| 彰武县| 巫山县| 双城市| 天柱县| 新河县| 宣恩县| 鹿泉市| 竹山县| 县级市| 德钦县| 泸水县| 阿克陶县| 永春县| 茌平县|