记使用STL与unique_ptr造成的事故-段子类比

   最近由于业务需要在写内存池子时遇到了一个doule-free的问题。折腾半个晚上以为自己的眼睛花了。开始以为是编译器有问题(我也是够自信的),但是在windows下使用qtcreator vs2017 和Linux下 使用gcc纷纷编译执行得到相同的结果。有一点要说的是使用gcc和qtcreator(mingW)虽然都double-free了,但是都没有给出错误的执行代码,vs在执行到析构函数时却可以给出给出异常提示。不得不说vs的运行检查更严格一些。下面我们先说正事后聊段子。 

下面先贴出代码,如果你能一眼看出问题,请在评论里叫我一声”弟弟“

#include <list>
#include <mutex>
#include <algorithm>
#include <memory>
#include <assert.h>
#include <iostream>

using namespace std;

template<typename T>
class DataObjectPool
{
public:
    DataObjectPool(){}
    ~DataObjectPool(){}
    
public:
    T* Get()
    {
        std::lock_guard<std::mutex> lockGuard(m_mutex);
        typename PtrList::iterator it = m_freeList.begin();
        std::unique_ptr<T>     memoryPtr;
        T*                                  pReturn = nullptr;
   
        if (it == m_freeList.end())
        {
            memoryPtr.reset(new T());
        }
        else
        {
            memoryPtr.reset((*it).release());
            m_freeList.pop_front();
        }
        
        pReturn = memoryPtr.get();
        m_usedList.push_back(std::move(memoryPtr));
        return pReturn;
    }
    
   void Free(T* pData)
    {
        std::lock_guard<std::mutex> lockGuard(m_mutex);
        
        std::unique_ptr<T>     memoryPtr;
        memoryPtr.reset(pData);
        
        auto itr = std::find(m_usedList.begin(),m_usedList.end(),memoryPtr);
        
        if(itr != m_usedList.end())
        {
            m_freeList.push_back(std::move(memoryPtr));
            m_usedList.erase(itr);  
        }
    }
private:
    typedef std::list<std::unique_ptr<T>> PtrList;
private:
    
    std::mutex   m_mutex;
    PtrList             m_usedList;
    PtrList             m_freeList;
};





class Test
{
public:
    Test()
    {
        cout << "Test()" << endl;
    }
    ~Test()
    {
        cout << "~Test()" << endl;
    }
};

int main(int argc, char *argv[])
{
    DataObjectPool<Test> PoolTest;
    auto pt1 = PoolTest.Get();
    auto pt2 = PoolTest.Get();
    PoolTest.Free(pt1);
    PoolTest.Free(pt2);
    
}

运行结果:

Test()
Test()
~Test()
~Test()
~Test()
~Test()
D:\workspace\Test\build-Test2-Desktop_Qt_5_8_0_MinGW_32bit-Debug\debug\Test2.exe exited with code 0

看代码太麻烦,这里画出逻辑图,肯定是Free过程中除了什么问题,代码对图再看一下。

分析:

1) memoryPtr通过reset方法获取到指针后通过std::move(memoryPtr)将指针转移了,所以它不会释放指针。

2) list::erase方法删除迭代器确实会释放释放相关内存,可是内存在释放之前不是已经std::move吗?

问题的关键就在这里:

  std::unique_ptr<T> memoryPtr 和 auto itr迭代器所包裹的指针是一样的,即都包裹了pData,可是他们根本不是一个东西。而是两个对象,看下面这段代码,可以更清楚:

#include <iostream>
#include <memory>
using namespace std;

class Test
{
public:
    Test()
    {
        cout<<"Test()"<<endl;
    }
    ~Test()
    {
        cout<<"~Test()"<<endl;
    }
};
int main()
{
    Test *t = new Test();

    unique_ptr<Test> upt1; //!依照上面的模型upt1可以看作是某个迭代器它包含t指针;
    upt1.reset(t);
    unique_ptr<Test> upt2; //!后来者对t也宣布了所有权
    upt2.reset(t);

    if(upt1 == upt2)
    {
        cout<<upt1.pointer()<<endl;
        cout<<"upt1==upt2"<<endl;
    }
}

    一个裸指针在丢给一个unique_ptr对象之前,该unique_ptr认为对该指针所有权是唯一的,当多个unique_ptr对象引用同一个裸的指针是无法检查该指针被谁引用,这一点,所以也double free也就不稀奇了。

  奇怪的是虽然std::unique_ptr是指针的所有权是独占的,指针只能转移,但是其却重载了operator ==运算符。既然是独占的,也就是std::unique_ptr不能在逻辑上承认多个unique_ptr对象对同一指针同时占有。这个操作让人非常疑惑。这也就是代码"auto itr = std::find(m_usedList.begin(),m_usedList.end(),memoryPtr)"能够返回有效迭代器和"if(upt1 == upt2)"能够成立的原因了。下面贴出std::unique_ptr "=="操作符重载的实现:

  其内部是对两个裸指针的比较,让人费解的实现。这一实现这让我联想到在一个一夫一妻制国家里,很多渣男(女)和多人保持实质上关系也算跟不上违法。

讲个段子,不为别的就为加深一下印象。

      这里double-free的模型可以用现实生活中的一个现象来映射:渣男A同时和B女和C女保持着关系,然而B和C并不知道对方的存在。A路过B的住处交完公粮,又去C处想去谈谈理想,结果C一时兴起也要求A交公粮,结果A却表现出了“重症鸡无力患者”该有的症状。绕不开的瓜,联系最近的娱乐新闻,罗志祥先生问鼎亚洲炮王。罗志祥先生异于常人,他是一个被std::share_ptr包裹的男人,所以多人运动也不在话下。我觉的这个问题与生活中的某些问题颇为相似,又有不同。人类社会果然更加复杂。

回归正题,原因弄明白了,我们来修改一下来让来避免这个问题:

推荐一篇文章:

C++ 智能指针的正确使用方式》总结的不错!

 https://www.cyhone.com/articles/right-way-to-use-cpp-smart-pointer/

 

 

 

 

 

 

 

猜你喜欢

转载自www.cnblogs.com/wangkeqin/p/12787548.html