防止或检测 "this" 在使用过程中被删除
Prevent or detect "this" from being deleted during use
我经常看到的一个错误是容器在循环访问时被清除。我试图整理一个小示例程序来演示这种情况。需要注意的一件事是,这经常会发生很多函数调用,因此很难检测到。
注意:此示例故意显示一些设计不佳的代码。我正在尝试找到一种解决方案来检测由编写此类代码引起的错误,而无需仔细检查整个代码库(~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 替换全局 new 和 delete / mprotect 并在空闲内存访问后检测使用。然而,如果向量不需要重新分配(即只删除了一些元素或新大小还不是保留大小),这和 Valgrind 有时无法拾取它。理想情况下,我不想克隆很多 STL 来制作一个 std::vector 版本,它总是在调试/测试期间重新分配每个 insertion/deletion。
一种几乎可行的方法是,如果 std::vector 改为包含 std::weak_ptr,则使用 .lock() 来创建临时引用可防止在 类 方法中执行时删除它。但是,这不能与 std::shared_ptr 一起使用,因为您不需要 lock() 并且与普通对象相同。仅仅为此创建一个弱指针容器是一种浪费。
还有谁能想出一种方法来保护自己免受这种情况的影响。
最简单的方法是 运行 您的单元测试与 Clang MemorySanitizer 链接。
让一些持续集成 Linux 框在每次推送时自动执行
进入回购协议。
MemorySanitizer 具有 "Use-after-destruction detection"(标志 -fsanitize-memory-use-after-dtor
+ 环境变量 MSAN_OPTIONS=poison_in_dtor=1
),因此它会破坏执行代码的测试,并使您的持续集成变红。
如果您既没有单元测试也没有持续集成,那么您也可以使用 MemorySanitizer 手动调试您的代码,但与最简单的方法相比,这很难。所以最好开始使用持续集成和编写单元测试。
请注意,在析构函数运行但内存尚未释放后,可能存在内存读写的合理原因。例如 std::variant<std::string,double>
。它允许我们分配它 std::string
然后 double
所以它的实现可能会破坏 string
并为 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
#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 并且应该没有性能下降。
在您的特定示例中,苦难归结为不少于两个设计缺陷:
- 你的向量是一个全局变量。尽可能限制所有对象的范围,这样的问题就不太可能发生。
- 考虑到单一责任原则,我很难想象如何想出需要的class ] 有一些方法可以直接或间接(可能通过 100 层调用堆栈)删除可能恰好是
this
. 的对象
我知道你的例子是人为的,故意不好,所以请不要误会我的意思:我敢肯定,在你的实际情况下,坚持一些基本的设计规则如何防止你不要这样做。但正如我所说,我坚信良好的设计会减少出现此类错误的可能性。事实上,我不记得我曾经遇到过这样的问题,但也许我的经验还不够 :)
然而,如果尽管坚持了一些设计规则,这仍然是一个问题,那么我有一个想法如何检测它:
- 在您的 class 中创建一个成员
int recursionDepth
并使用 0
对其进行初始化
- 在每个非私有方法的开头递增它。
- 使用 RAII 确保在每个方法结束时再次递减
- 在析构函数中检查为
0
,否则说明析构函数被this
. 的某种方法直接或间接调用
- 您可能想要
#ifdef
所有这些并仅在调试版本中启用它。这实际上会使它成为一个调试断言,有些人喜欢他们:)
请注意,这在多线程环境中不起作用。
我经常看到的一个错误是容器在循环访问时被清除。我试图整理一个小示例程序来演示这种情况。需要注意的一件事是,这经常会发生很多函数调用,因此很难检测到。
注意:此示例故意显示一些设计不佳的代码。我正在尝试找到一种解决方案来检测由编写此类代码引起的错误,而无需仔细检查整个代码库(~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 替换全局 new 和 delete / mprotect 并在空闲内存访问后检测使用。然而,如果向量不需要重新分配(即只删除了一些元素或新大小还不是保留大小),这和 Valgrind 有时无法拾取它。理想情况下,我不想克隆很多 STL 来制作一个 std::vector 版本,它总是在调试/测试期间重新分配每个 insertion/deletion。
一种几乎可行的方法是,如果 std::vector 改为包含 std::weak_ptr,则使用 .lock() 来创建临时引用可防止在 类 方法中执行时删除它。但是,这不能与 std::shared_ptr 一起使用,因为您不需要 lock() 并且与普通对象相同。仅仅为此创建一个弱指针容器是一种浪费。
还有谁能想出一种方法来保护自己免受这种情况的影响。
最简单的方法是 运行 您的单元测试与 Clang MemorySanitizer 链接。 让一些持续集成 Linux 框在每次推送时自动执行 进入回购协议。
MemorySanitizer 具有 "Use-after-destruction detection"(标志 -fsanitize-memory-use-after-dtor
+ 环境变量 MSAN_OPTIONS=poison_in_dtor=1
),因此它会破坏执行代码的测试,并使您的持续集成变红。
如果您既没有单元测试也没有持续集成,那么您也可以使用 MemorySanitizer 手动调试您的代码,但与最简单的方法相比,这很难。所以最好开始使用持续集成和编写单元测试。
请注意,在析构函数运行但内存尚未释放后,可能存在内存读写的合理原因。例如 std::variant<std::string,double>
。它允许我们分配它 std::string
然后 double
所以它的实现可能会破坏 string
并为 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
#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 并且应该没有性能下降。
在您的特定示例中,苦难归结为不少于两个设计缺陷:
- 你的向量是一个全局变量。尽可能限制所有对象的范围,这样的问题就不太可能发生。
- 考虑到单一责任原则,我很难想象如何想出需要的class ] 有一些方法可以直接或间接(可能通过 100 层调用堆栈)删除可能恰好是
this
. 的对象
我知道你的例子是人为的,故意不好,所以请不要误会我的意思:我敢肯定,在你的实际情况下,坚持一些基本的设计规则如何防止你不要这样做。但正如我所说,我坚信良好的设计会减少出现此类错误的可能性。事实上,我不记得我曾经遇到过这样的问题,但也许我的经验还不够 :)
然而,如果尽管坚持了一些设计规则,这仍然是一个问题,那么我有一个想法如何检测它:
- 在您的 class 中创建一个成员
int recursionDepth
并使用0
对其进行初始化
- 在每个非私有方法的开头递增它。
- 使用 RAII 确保在每个方法结束时再次递减
- 在析构函数中检查为
0
,否则说明析构函数被this
. 的某种方法直接或间接调用
- 您可能想要
#ifdef
所有这些并仅在调试版本中启用它。这实际上会使它成为一个调试断言,有些人喜欢他们:)
请注意,这在多线程环境中不起作用。