C++ 防止或检测;这";避免在使用过程中被删除

C++ 防止或检测;这";避免在使用过程中被删除,c++,memory,stl,containers,unsafe,C++,Memory,Stl,Containers,Unsafe,我经常看到的一个错误是,在遍历容器时,容器被清除。我试着编写一个小的示例程序来演示这种情况。需要注意的一点是,这种情况经常发生在许多函数调用的深处,因此很难检测到 注意:这个示例故意显示了一些设计糟糕的代码。我试图找到一个解决方案来检测编写代码所造成的错误,而不必仔细检查整个代码库(~ 500个C++单元)< /P>。 #包括 #包括 #包括 类炸弹; 传播媒介炸弹; 阶级炸弹 { std::字符串名; 公众: 炸弹(标准::字符串名称) { 此->名称=名称; } 无效接触() { 如果(ra

我经常看到的一个错误是,在遍历容器时,容器被清除。我试着编写一个小的示例程序来演示这种情况。需要注意的一点是,这种情况经常发生在许多函数调用的深处,因此很难检测到

注意:这个示例故意显示了一些设计糟糕的代码。我试图找到一个解决方案来检测编写代码所造成的错误,而不必仔细检查整个代码库(~ 500个C++单元)< /P>。
#包括
#包括
#包括
类炸弹;
传播媒介炸弹;
阶级炸弹
{
std::字符串名;
公众:
炸弹(标准::字符串名称)
{
此->名称=名称;
}
无效接触()
{
如果(rand()%100>30)
{
/*模拟所有正在爆炸的东西*/
炸弹。清除();
/*错误:“this”不再有效*/

std::cout最简单的方法是使用链接的插件运行单元测试。 让一些持续集成Linux box在每次推送时自动完成 回购


MemorySanizer具有“销毁后使用检测”(标志
-fsanizize memory Use after dtor
+环境变量
MSAN_OPTIONS=poison_in_dtor=1
),因此它将炸毁执行代码的测试,并将持续集成变为红色

如果您既没有单元测试也没有连续集成,那么您也可以使用MemorySinitizer手动调试代码,但与最简单的方法相比,这是一种困难的方法。因此,最好开始使用连续集成并编写单元测试


请注意,在运行析构函数后,内存读写可能有正当的原因,但内存尚未释放。例如
std::variant
。它允许我们分配
std::string
,然后
double
,因此它的实现可能会破坏
字符串
,并将相同的存储重新用于
double
。很遗憾,目前过滤此类案例是手工操作,但工具不断发展。

最后,我使用了一个自定义迭代器,如果所有者std::vector在迭代器仍在作用域内时调整大小,它将记录错误或中止(给我一个程序堆栈跟踪).这个例子有点复杂,但我尽量简化它,并从迭代器中删除了未使用的功能

这个系统已经标记了大约50个这种性质的错误。有些可能是重复的。但是Valgrind和ElecricFence在这一点上得到了清晰的结果,这是令人失望的(他们总共标记了大约10个错误,这是我自代码清理开始以来已经修复的)

在本例中,我使用clear()哪个Valgrind没有标记为错误。但是在实际的代码库中,我需要检查的是随机访问擦除(即vec.erase(vec.begin()+9)),Valgrind不幸错过了很多

main.cpp

#include "sstd_vector.h"

#include <iostream>
#include <string>
#include <memory>

class Bomb;

sstd::vector<std::shared_ptr<Bomb> > bombs;

class Bomb
{
  std::string name;

public:
  Bomb(std::string name)
  {
    this->name = name;
  }

  void touch()
  {
    if(rand() % 100 > 30)
    {
      /* Simulate everything being exploded! */
      bombs.clear(); // Causes an ABORT

      std::cout << "Crickey! The bomb was set off by " << name << std::endl;
    }
  }
};

int main()
{
  bombs.push_back(std::make_shared<Bomb>("Freddy"));
  bombs.push_back(std::make_shared<Bomb>("Charlie"));
  bombs.push_back(std::make_shared<Bomb>("Teddy"));
  bombs.push_back(std::make_shared<Bomb>("Trudy"));

  /* The key part is the lifetime of the iterator. If the vector
   * changes during the lifetime of the iterator, even if it did
   * not reallocate, an error will be logged */
  for(sstd::vector<std::shared_ptr<Bomb> >::iterator it = bombs.begin(); it != bombs.end(); it++)
  {
    it->get()->touch();
  }

  return 0;
}
#包括“sstd_vector.h”
#包括
#包括
#包括
类炸弹;
sstd::矢量炸弹;
阶级炸弹
{
std::字符串名;
公众:
炸弹(标准::字符串名称)
{
此->名称=名称;
}
无效接触()
{
如果(rand()%100>30)
{
/*模拟所有正在爆炸的东西*/
bombs.clear();//导致中止
标准::cout(0)
{
/*报告错误或中止*/
中止();
}
}
公众:
vector():refs(0){}
~vector()
{
检查_valid();
}
向量和运算符=(向量常量和其他)
{
检查_valid();
数据=其他数据;
归还*这个;
}
无效推回(T val)
{
检查_valid();
数据。推回(val);
}
无效清除()
{
检查_valid();
data.clear();
}
类迭代器
{
友元类向量;
typename std::vector::iterator;
向量*父;
迭代器(){}
迭代器和运算符=(迭代器常量&){abort();}
公众:
迭代器(迭代器常量和其他)
{
it=other.it;
parent=other.parent;
父->参考文件++;
}
~iterator()
{
父->参考--;
}
布尔运算符!=(迭代器常量和其他)
{
如果(it!=other.it)返回true;
如果(parent!=other.parent)返回true;
返回false;
}
迭代器运算符++(int-val)
{
迭代器rtn=*这个;
it++;
返回rtn;
}
T*运算符->()
{
返回(*it);
}
T&运算符*()
{
归还它;
}
};
迭代器begin()
{
迭代器rtn;
rtn.it=data.begin();
rtn.parent=这个;
参考文献++;
返回rtn;
}
迭代器结束()
{
迭代器rtn;
rtn.it=data.end();
rtn.parent=这个;
参考文献++;
返回rtn;
}
};
}
这个系统的缺点是我必须使用迭代器,而不是.at(idx)[idx]。我个人不太介意这个。如果需要随机访问,我仍然可以使用.begin()+idx


它稍微慢一点(与Valgrind相比没有什么)。当我完成后,我可以用std::vector搜索/替换sstd::vector,并且应该不会有性能下降。

在您的特定示例中,痛苦归结为不少于两个设计缺陷:

  • 向量是一个全局变量。尽可能限制所有对象的范围,这样的问题就不太可能发生
  • 考虑到单一责任原则,我很难想象一个类怎么会需要某种方法来直接或间接(可能通过100层调用堆栈)删除可能是
    this
    的对象
  • 我知道你的例子是人为的,故意的不好,所以请不要误解我的意思:我确信在你的实际案例中,坚持一些基本的设计规则如何阻止你这样做并不那么明显。但正如我所说的,我坚信好的设计会降低成本
    #include "sstd_vector.h"
    
    #include <iostream>
    #include <string>
    #include <memory>
    
    class Bomb;
    
    sstd::vector<std::shared_ptr<Bomb> > bombs;
    
    class Bomb
    {
      std::string name;
    
    public:
      Bomb(std::string name)
      {
        this->name = name;
      }
    
      void touch()
      {
        if(rand() % 100 > 30)
        {
          /* Simulate everything being exploded! */
          bombs.clear(); // Causes an ABORT
    
          std::cout << "Crickey! The bomb was set off by " << name << std::endl;
        }
      }
    };
    
    int main()
    {
      bombs.push_back(std::make_shared<Bomb>("Freddy"));
      bombs.push_back(std::make_shared<Bomb>("Charlie"));
      bombs.push_back(std::make_shared<Bomb>("Teddy"));
      bombs.push_back(std::make_shared<Bomb>("Trudy"));
    
      /* The key part is the lifetime of the iterator. If the vector
       * changes during the lifetime of the iterator, even if it did
       * not reallocate, an error will be logged */
      for(sstd::vector<std::shared_ptr<Bomb> >::iterator it = bombs.begin(); it != bombs.end(); it++)
      {
        it->get()->touch();
      }
    
      return 0;
    }
    
    #include <vector>
    
    #include <stdlib.h>
    
    namespace sstd
    {
    
    template <typename T>
    class vector
    {
      std::vector<T> data;
      size_t refs;
    
      void check_valid()
      {
        if(refs > 0)
        {
          /* Report an error or abort */
          abort();
        }
      }
    
    public:
      vector() : refs(0) { }
    
      ~vector()
      {
        check_valid();
      }
    
      vector& operator=(vector const& other)
      {
        check_valid();
        data = other.data;
    
        return *this;
      }
    
      void push_back(T val)
      {
        check_valid();
        data.push_back(val);
      }
    
      void clear()
      {
        check_valid();
        data.clear();
      }
    
      class iterator
      {
        friend class vector;
    
        typename std::vector<T>::iterator it;
        vector<T>* parent;
    
        iterator() { }
        iterator& operator=(iterator const&) { abort(); }
    
      public:
        iterator(iterator const& other)
        {
          it = other.it;
          parent = other.parent;
          parent->refs++;
        }
    
        ~iterator()
        {
          parent->refs--;
        }
    
        bool operator !=(iterator const& other)
        {
          if(it != other.it) return true;
          if(parent != other.parent) return true;
    
          return false;
        }
    
        iterator operator ++(int val)
        {
          iterator rtn = *this;
          it ++;
    
          return rtn;
        }
    
        T* operator ->()
        {
          return &(*it);
        }
    
        T& operator *()
        {
          return *it;
        }
      };
    
      iterator begin()
      {
        iterator rtn;
    
        rtn.it = data.begin();
        rtn.parent = this;
        refs++;
    
        return rtn;
      }
    
      iterator end()
      {
        iterator rtn;
    
        rtn.it = data.end();
        rtn.parent = this;
        refs++;
    
        return rtn;
      }
    };
    
    }