我经常看到的一个错误是在迭代它时清除容器。我试图整理一个小例子程序来证明这种情况。需要注意的一点是,这通常会发生很多深度函数调用,因此很难检测到。
注意:此示例故意显示一些设计不良的代码。我试图找到一个解决方案,以检测编写这样的代码所导致的错误,而无需仔细检查整个代码库(~500 C ++单位)
#include <iostream>
#include <string>
#include <vector>
class Bomb;
std::vector<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();
/* An error: "this" is no longer valid */
std::cout << "Crickey! The bomb was set off by " << name << std::endl;
}
}
};
int main()
{
bombs.push_back(Bomb("Freddy"));
bombs.push_back(Bomb("Charlie"));
bombs.push_back(Bomb("Teddy"));
bombs.push_back(Bomb("Trudy"));
for(size_t i = 0; i < bombs.size(); i++)
{
bombs.at(i).touch();
}
return 0;
}
有人可以建议一种保证这种情况不会发生的方法吗? 我目前能够检测到此类事情的唯一方法是将全局新和删除替换为 mmap / mprotect 并在空闲内存访问后检测使用。然而,如果向量不需要重新分配(即只删除了一些元素或者新大小还不是保留大小),这和Valgrind有时无法获取它。理想情况下,我不希望克隆大部分STL来创建一个std :: vector版本,它总是在调试/测试期间重新分配每个插入/删除。
一种几乎可行的方法是,如果 std :: vector 包含 std :: weak_ptr ,那么 .lock()的使用创建临时引用可防止在执行时在类方法中删除它。但是,这不适用于 std :: shared_ptr ,因为您不需要 lock(),并且与普通对象相同。为此创建一个弱指针容器将是浪费。
任何人都可以想办法保护自己免受这种伤害。
答案 0 :(得分:3)
最简单的方法是在链接到Clang MemorySanitizer的情况下运行单元测试。 让一些持续集成的Linux机箱在每次推送时自动完成 回购。
MemorySanitizer具有&#34;破坏后使用检测&#34; (标记-fsanitize-memory-use-after-dtor
+环境变量MSAN_OPTIONS=poison_in_dtor=1
)因此它会破坏执行代码的测试,并使持续集成变为红色。
如果您既没有单元测试也没有持续集成,那么您也可以使用MemorySanitizer手动调试代码,但这与最简单的方法相比很难。因此,最好开始使用持续集成和编写单元测试。
请注意,在运行析构函数但内存尚未释放后,可能存在内存读取和写入的合理原因。例如std::variant<std::string,double>
。它允许我们分配std::string
然后double
,因此其实现可能会破坏string
并为double
重用相同的存储空间。不幸的是,过滤此类案例是目前的手工工作,但工具也在不断发展。
答案 1 :(得分:1)
在你的特定例子中,苦难归结为不少于两个设计缺陷:
this
的对象。我知道你的例子是人为的,故意不好的,所以请不要误解我的意思:我确信在你的实际案例中,如何坚持一些基本的设计规则会阻止你这样做。但正如我所说,我坚信良好的设计将减少这些错误的可能性。事实上,我不记得我曾经遇到过这样的问题,但也许我只是没有足够的经验:)
然而,如果这仍然是一个问题,尽管坚持一些设计规则,那么我有这个想法如何检测它:
int recursionDepth
,并使用0
0
,否则意味着通过this
的某种方法直接或间接调用析构函数。#ifdef
所有这些并仅在调试版本中启用它。这基本上使它成为调试断言,有些人喜欢它们:)请注意,这在多线程环境中不起作用。
答案 2 :(得分:0)
最后我使用了一个自定义迭代器,如果所有者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 强>
#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;
}
};
}
这个系统的缺点是我必须使用迭代器而不是 .at(idx)或 [idx] 。我个人不太介意这个。如果需要随机访问,我仍然可以使用 .begin()+ idx 。
它有点慢(虽然与Valgrind相比没什么)。当我完成后,我可以用 std :: vector 搜索/替换 sstd :: vector ,并且不应该有性能下降。