假设我们有一个简单的结构:
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++ 语言功能来做到这一点?
这意味着如果通过显式调用析构函数销毁该类型的对象(但其内存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 的另一个原因。