这是我的一般性问题:使用正在被销毁的派生类指针从基类析构函数调用非虚基类成员函数是否安全?
让我用下面的例子来解释这一点。
我有一个 Base 类和一个派生的 Key 类。
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; }
};
我现在创建一个 std::map(std::set 也可以)派生 Key 类指针按它们的 < em>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*, int, 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 编译器时没有看到任何错误,请问您能否告知此代码是否安全?
谢谢,
瓦伦
编辑:我刚刚意识到 Comparator2 可以通过直接使用 Base 类指针进一步简化,如下所示:
struct Comparator2
{
using is_transparent = std::true_type;
bool operator()(const Base *k1, const Base *k2) const
{
return k1->getId() < k2->getId();
}
};
最佳答案
除非我误解了你的代码,否则这基本上与具有 self 破坏功能的对象相同(例如 delete this;)
- 这是合法的 - 前提是你在依赖于你的删除后什么都不做对象存在 - 如调用成员函数或访问成员变量等...
所以看看你的代码,我认为你没问题 - 如果你再次使用它,你指向对象的指针现在是 UB,并且返回函数调用堆栈看起来很安全。
但我强烈建议另一种方法 - 这很可能是维护的噩梦 - 如果毫无戒心的开发人员后来更改此代码,他们很可能会导致 UB。
UnholySheep 关于一个单独的类来为您管理所有这些的想法听起来好多了:)
更新
你在这里真正做的是调用一个普通函数(notify()
),它又调用成员(非虚拟)getId()
通过比较器功能通过 map.erase/find 功能。这一切都发生在析构函数范围内——这很好。这是调用 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()
) 调用都在基类 d'tor 函数中完成 - 这是安全的。
为了让您不必编写“异构比较器”(Comparitor2),并使您的设计/工作更轻松,我可能建议让您的 map 使用基类指针:std::map<const Base*, int, Comparator1> myMap;
然后你可以摆脱你的 Comparitor2 结构,你可以使用 map.erase(b)
直接在你的notify()
功能,所有这些都变得更清晰/更清晰。这是一个带有一些注释(打印)的示例:https://godbolt.org/z/h5zTc9
关于c++ - 使用派生类指针从基类析构函数调用非虚拟基类成员函数是否安全?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64401178/