使用派生类指针从基类析构函数调用非虚基类成员函数安全吗?

3

我的一般问题是:使用正在被销毁的派生类指针从基类析构函数调用非虚基类成员函数是否安全

让我通过以下示例进行解释。

我有一个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; }                                                                                                                    
};

现在我创建了一个派生自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;

当一个被删除时,我希望从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的第四种重载形式,在地图中查找键,然后将该迭代器传递给erase,如下所示:
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 所以我的第二个版本是否安全,使用了Comparator2std::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


2
有点相关,但整个架构似乎非常不可靠和脆弱。为什么需要在析构函数调用中处理这个?在我看来,最好将Key的使用抽象化,让用户通过某个管理类(该类将是唯一允许创建和删除实例的类),也许结合某种智能指针来使用。 - UnholySheep
1
这似乎过于复杂了。预期的使用情况是什么? - super
我正在处理一个依赖于这种框架的巨大代码库。有数百个类都是从一个单一的_Base_类派生而来,我们在代码中使用了成千上万次的映射和集合,这些映射和集合使用_Derived_类指针作为键。在大多数情况下,我们都有一些管理器或观察者负责清理这些映射/集合,但并不总是一致地完成。我希望能够设计一个更通用的包装器,可以自动使用从_Base_类析构函数触发的通知进行清理现有的映射/集合。 - Varun Hiremath
@VarunHiremath 我认为你的用例不会改变结果,这不是一个好的解决方案,但如果你认为风险与重构所需时间相比是值得的,那就由你决定。你可以在对象成员函数调用内部删除对象,但你需要非常小心。如果你要使用这种方法(特别是因为你的项目听起来很大/多人),你需要让其他开发人员非常清楚,在编辑此区域的代码时要非常小心,以确保在调用此通知后不再访问成员函数/变量等。 - code_fodder
嗨@code_fodder,感谢您的回复!虽然我知道从Base类dtor发送的通知与此指针不安全,例如在这篇文章中清楚地解释了调用_virtual function_、_typeid_、_dynamic_cast_等是不安全的。但在这种特殊情况下,我只是使用了来自Base类的非虚拟方法。所以我想知道这是否100%安全(根据C ++标准),或者在运行时是否也可能导致一些UB? - Varun Hiremath
@VarunHiremath 我会在下面的答案中添加更新... - code_fodder
1个回答

1
除非我误解了你的代码,否则这基本上与具有销毁自身功能的对象相同(例如delete this;)- 这是合法的 - 前提是您在删除后不执行任何依赖于您的对象存在的操作,如调用成员函数或访问成员变量等...
因此,从您的代码来看,我认为您没问题了 - 如果您再次使用指向该对象的指针,则该指针现在已经是未定义行为,而且函数调用栈的返回看起来是安全的。
但我强烈建议采用另一种方法 - 这很可能会成为维护噩梦 - 如果以后有一个毫不知情的开发人员更改了这个代码,他们很可能会导致未定义行为。
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 类的析构函数中完成 - 这是安全的。
我建议你使用基类指针来使你的设计/工作更加轻松,这样你就不必编写你的 "异类比较器" (Comparitor2),并且可以让你的映射使用基类指针:std::map<const Base*, int, Comparator1> myMap; 然后你就可以在你的 notify() 函数中直接使用 map.erase(b),这样所有的内容都变得更加清晰简洁。这里有一个带有一些注释的示例 (打印): https://godbolt.org/z/h5zTc9

嗨 @code_fodder,谢谢解释。我同意,如果使用 Base 类指针构建地图,则毫无疑问一切都很好。但是我们需要使用 Key 指针构建地图,因为我们同时使用 KeyValue_。因此,_歧义 真正源于在地图中使用 Key 类指针。如果您使用我的修改后的 Comparator2 进行 finderase 方法,您将注意到调用堆栈完全相同。唯一的问题是,由于在我们到达 ~Base()~Key() 已经完成,因此取消引用 Key 指针并调用 getId() 是否仍然有效? - Varun Hiremath
是的,这是安全的 :) - 通常在基类中有一些工作要做(比如关闭“东西”),甚至可以隐藏起来不让派生类知道。我没有完全理解你关于必须访问Key的观点 - 如果你的映射是一个base的映射,你仍然可以这样做 - 但是你将需要静态(或者如果未知,则动态)转换 - 所以这可能会变得不太好 - 取决于你的代码 - 所以这是你的决定 :) - code_fodder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接