【发布时间】:2021-01-31 16:26:56
【问题描述】:
这是我的一般性问题:使用正在销毁的派生类指针从基类析构函数调用非虚拟基类成员函数是否安全?
让我通过下面的例子来解释一下。
我有一个 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 类指针按它们的 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
}
所以为了规避这个问题,我定义了一个异构 Comparator 并使用 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();
}
};
【问题讨论】:
-
有点相关,但整个架构看起来非常粗略和脆弱。为什么你需要在析构函数调用中处理这个? IMO 最好将
Key的用法从某个管理器类后面的用户中抽象出来(这将是唯一允许创建和删除实例的类),也许与某种形式的智能指针结合使用 -
这似乎过于复杂。预期的用例是什么?
-
我正在开发一个依赖于这种框架的庞大代码库。有数百个类派生自一个 Base 类,我们在代码中使用 Derived 类指针作为键的数千个地方使用了映射和集合。在大多数地方,我们都有一些经理或观察员负责清理这些地图/集合,但并没有始终如一地完成。我希望在现有的地图/集合上提供一个更通用的包装器,使用 Base 类析构函数触发的通知自动清理自身。
-
@VarunHiremath 我不认为您的用例会改变结果,这不是一个好的解决方案,但如果您认为风险与时间的重构是值得的,那么这取决于您。您可以从对象成员函数调用中删除对象 - 但您需要非常小心。如果您打算使用这种类型的方法(特别是因为您的项目听起来很大/多人),您需要让任何其他开发人员非常清楚地非常小心地编辑该区域的代码,以便成员函数/变量/等。调用此通知后不访问...
-
嗨@code_fodder,感谢您的回复!虽然我知道使用此指针从基类 dtor 发送的通知使用起来并不安全。例如,在这个article 中,清楚地解释了调用 virtual function、typeid、dynamic_cast 等是不安全的。但在这种特殊情况下,我只是使用 Base 类中的非虚拟方法。所以我想知道这是否 100% 安全(根据 C++ 标准)或者这是否也会导致运行时出现一些 UB?
标签: c++ pointers c++14 derived-class base-class