问题描述
这是我的一般问题:使用将被破坏的派生类指针从基类析构函数中调用非虚拟基类成员函数是否安全?
让我通过以下示例对此进行解释。
static unsigned int count = 0;
class Base;
class Key;
void notify(const Base *b);
class Base
{
public:
Base(): id(count++) {}
virtual ~Base() { notify(this); }
int getId() const { return id; }
virtual int dummy() const = 0;
private:
unsigned int id;
};
class Key : public Base
{
public:
Key() : Base() {}
~Key() {}
int dummy() const override { return 0; }
};
我现在创建一个派生的 Key 类指针的 std :: map ( std :: set 也可以使用),这些指针按其 id 如下:
struct Comparator1
{
bool operator()(const Key *k1,const Key *k2) const
{
return k1->getId() < k2->getId();
}
};
std::map<const Key*,int,Comparator1> myMap;
现在,当删除 Key 时,我想从myMap中删除该密钥。为此,我首先尝试实现从〜Base()触发的 notify 方法,如下所示,但是我知道这是不安全的,并且可能导致不确定的行为。我已经在这里验证过:http://coliru.stacked-crooked.com/a/4e6cd86a9706afa1
void notify(const Base* b)
{
myMap.erase(static_cast<const Key *>(b)); //not safe,results in UB
}
因此,为了规避此问题,我定义了一个异构比较器,并使用std::map::find的变体(4)在映射中找到键,然后将该迭代器传递给擦除,如下所示:
struct Comparator2
{
using is_transparent = std::true_type;
bool operator()(const Key *k1,const Key *k2) const
{
return k1->getId() < k2->getId();
}
bool operator()(const Key *k1,const Base *b1) const
{
return k1->getId() < b1->getId();
}
bool operator()(const Base *b1,const Key *k1) const
{
return b1->getId() < k1->getId();
}
};
std::map<const Key*,Comparator2> myMap;
void notify(const Base* b)
{
// myMap.erase(static_cast<const Key *>(b)); //not safe,results in UB
auto it = myMap.find(b);
if (it != myMap.end())
myMap.erase(it);
}
我已经用g ++和clang测试了第二个版本,但没有看到任何未定义的行为。您可以在此处尝试代码:http://coliru.stacked-crooked.com/a/65f6e7498bdf06f7
那么使用 Comparator2 和 std :: map :: find 的第二个版本安全吗?在 Comparator2 中,我仍在使用指向其析构函数已被调用的派生 Key 类的指针。使用g ++或clang编译器没有看到任何错误,因此,请您告知这段代码是否安全?
谢谢
Varun
编辑:我刚刚意识到, Comparator2 可以通过直接使用 Base 类指针进一步简化,如下所示:
struct Comparator2
{
using is_transparent = std::true_type;
bool operator()(const Base *k1,const Base *k2) const
{
return k1->getId() < k2->getId();
}
};
这也适用:http://coliru.stacked-crooked.com/a/c7c10c115c20f5b6
解决方法
除非我对您的代码有误解,否则它基本上与具有破坏自身功能的对象相同(例如,delete this;)
-是合法的-只要您删除依赖于内容的内容后不执行任何操作存在于您的对象上-例如调用成员函数或访问成员变量等...
因此,查看您的代码,我认为您还可以-如果您再次使用该对象,则指向该对象的指针现在是UB,并且返回的函数调用堆栈看起来很安全。
但是我强烈建议另一种方法-这很可能是维护的噩梦-如果一个毫无戒心的开发人员后来更改此代码,他们很可能会导致UB。
UnholySheep设计一个单独的类来为您管理所有这些的想法听起来要好得多:)
更新
您在这里真正要做的就是调用一个普通函数(notify()
),该函数进而通过比较函数通过map.erase / find调用成员(非虚拟)getId()
函数。所有这些都发生在析构函数的作用域之内-很好。这是调用delete时发生的大致通话记录:
~Base()
|
v
notify()
|
v
Comparator() // This happens a number of times
|
v
getId() // This is called by Comparator
|
+----+
|
v
~Base() // base destructor returns
因此,您可以看到所有成员(getId()
)调用都是在Base类d'tor函数内完成的,这很安全。
为了避免不必要编写“异构比较器”(Comparitor2),并使您的设计/工作变得更简单,我建议您使用基类指针:std::map<const Base*,int,Comparator1> myMap;
,然后您可以摆脱Comparitor2结构,而可以直接在map.erase(b)
函数中使用notify()
,这一切将变得更加清晰。这是带有一些注释(打印)的示例:https://godbolt.org/z/h5zTc9