派生类销毁后使用基类成员

问题描述 投票:0回答:2

假设我们有一个简单的结构:

struct RefCounters {
    size_t strong_cnt;
    size_t weak_cnt;
    RefCounters() : strong_cnt(0), weak_cnt(0) {}
};

从实现的角度来看,析构函数

RefCounters::~RefCounters
应该什么都不做,因为它的所有成员都有原始类型。这意味着如果这种类型的对象被显式调用析构函数销毁(但它的内存是 not 释放),那么我们将能够在对象死亡后正常使用它的成员。

现在假设我们还有一些派生自

RefCounters
的类。假设
RefCounters
Derived
类的基类中恰好出现一次。假设为类
Derived
的对象显式调用析构函数,但它的内存 not 释放。之后可以访问成员
strong_cnt
weak_cnt
吗?

从实现的角度来说,应该是可以的,至少在不涉及虚拟继承的情况下。因为

Derived*
可以静态转换为
RefCounters*
(将编译时常量偏移量添加到地址),并且
RefCounters
的内存不应该被
Derived
类的析构函数触及。

这里是一个代码示例:

struct RefCounted : public RefCounters {
    virtual ~RefCounted() {}
};

struct Base : public RefCounted {
    int val1;
    virtual void print();
};

struct Derived : public Base {
    std::string val2;
    virtual void print();
};

Derived *pDer = new Derived();
pDer->~Derived();          //destroy object
pDer->strong_cnt++;        //modify its member
std::cout << pDer->strong_cnt << pDer->weak_cnt << "\n";

这样的代码是否被 C++ 标准视为未定义行为?有什么实际原因导致它无法工作吗?是否可以通过微小的更改或添加一些限制使其合法?

附言据推测,这样的代码示例允许进行 intrusive_ptr + weak_ptr 组合,这样如果至少有一个 weak_ptr 仍然指向它,那么 weak_ptr 总是可以从对象指针获得。 这个问题中的更多细节。

c++ language-lawyer destructor
2个回答
0
投票

我认为你的方法不好。评论中有一个很好的链接,显示了关于标准细节的争论。一旦有争论,不同的编译器很可能会以不同的方式实现这个细节。更。同一个编译器可能会将其实现从一个版本更改为另一个版本。

各种暗角用得越多,出问题的几率就越大

底线。愿意达成什么?为什么不能使用普通的 C++ 语言功能来做到这一点?


0
投票

这意味着如果通过显式调用析构函数销毁该类型的对象(但其内存not释放),那么我们将能够在对象死亡后正常使用其成员。

这个前提是不正确的,这与你的其余问题一样;当通过指向派生类类型的指针访问这些成员时,它肯定也不会被明确定义。

要了解您的前提为何不正确,让我们看一下 C++14 标准(在提出问题时生效的标准)。根据 [class.cdtor]/3:

[...] 要形成一个指向(或访问其值)对象

obj
的直接非静态成员的指针,
obj
的构造应该已经开始并且它的销毁应该没有完成,否则指针值的计算(或访问成员值)导致未定义的行为。

RefCounters
的析构函数是微不足道的这一事实并不为此规则提供例外。但是,它确实为 [class.cdtor]/1:

提供了一个例外

对于具有非平凡构造函数的对象,引用该对象的任何非静态成员或基类 在构造函数开始执行之前导致未定义的行为。对于具有非平凡性的对象 析构函数,在析构函数完成后引用对象的任何非静态成员或基类 执行导致未定义的行为。

对于一个被销毁的

RefCounters
对象,其内存还没有被释放或重用,明确定义为仅引用
strong_cnt
weak_cnt
成员,但未定义形成指向成员或成员的指针尝试访问它的值。这个在[basic.life]/5:

中也有提到

[...] 或者,在对象的生命周期结束之后,对象占用的存储空间被删除之前 重用或释放,任何指向对象将要或曾经位于的存储位置的指针 可以使用,但仅限于有限的方式。对于正在构建或破坏的对象,请参见 12.7。 [...] 允许通过此类指针进行间接访问,但生成的左值只能用于 有限的方式,如下所述。如果出现以下情况,则程序具有未定义的行为:

  • [...]
  • 指针用于访问非静态数据成员或调用非静态成员函数 对象,或
  • [...]

请注意,在 C++20 中,已明确即使破坏标量类型也会结束其生命周期。所以当你销毁一个

RefCounters
对象时,这也会销毁它的
size_t
成员,并且他们可能不再被访问。这给出了您的代码具有 UB 的另一个原因。

© www.soinside.com 2019 - 2024. All rights reserved.