【问题标题】:Is it safe to call a non-virtual base class member function from the base class destructor using a derived class pointer?使用派生类指针从基类析构函数调用非虚拟基类成员函数是否安全?
【发布时间】: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::mapstd::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

那么我使用 Comparator2std::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();                                                                                                                         
  }                                                                                                                                                                                                                                                                                      
};

这也有效:http://coliru.stacked-crooked.com/a/c7c10c115c20f5b6

【问题讨论】:

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

标签: c++ pointers c++14 derived-class base-class


【解决方案1】:

除非我误解了您的代码,否则这与具有破坏自身的功能的对象基本相同(例如 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()) 调用都是在基类 d'tor 函数中完成的 - 这是安全的。

我可能建议您不必编写“异构比较器”(Comparitor2),并使您的设计/工作更容易,让您的地图使用基类指针:std::map&lt;const Base*, int, Comparator1&gt; myMap; 然后您可以摆脱您的 Comparitor2 结构,您可以直接在您的 notify() 函数中使用 map.erase(b) ,这一切都变得更加清晰/清晰。这是一个带有一些注释(打印)的示例:https://godbolt.org/z/h5zTc9

【讨论】:

  • 嗨@code_fodder,感谢您的解释。我同意如果我使用 Base 类指针构建地图,那么毫无疑问一切都很好。但是我们需要使用 Key 指针来构建地图,因为我们同时使用了 KeyValue。所以 ambiguity 真正源于地图中 Key 类指针的使用。如果您将我修改后的 Comparator2finderase 方法一起使用,您会注意到调用堆栈完全相同。唯一的问题是,由于 ~Key() 在我们到达 ~Base() 时已经完成,所以取消引用 Key 指针并调用getId() 还有效吗?
  • 是的,这是安全的 :) - 通常在基类中有一些工作要做(比如关闭“东西”),甚至可以隐藏在派生类之外。我不太明白您关于必须访问Key 的观点-如果您的地图是base 的地图,您仍然可以这样做-但是您将需要静态(或者如果未知,则为动态)强制转换-所以这最终可能会变得不那么好 - 取决于你的代码 - 这就是你的电话:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-24
  • 2011-09-27
  • 2015-05-09
  • 2015-04-16
  • 2021-08-22
相关资源
最近更新 更多