1

Suppose that we have a simple struct:

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

From implementation point, the destructor RefCounters::~RefCounters should do nothing, since all its members have primitive type. This means that if an object of this type is destroyed with explicit call of destructor (but its memory is not deallocated), then we would be able to work with its members normally after the object is dead.

Now suppose that we have some more classes derived from RefCounters. Suppose that RefCounters is present exactly once among base classes of Derived class. Suppose that destructor is called explicitly for an object of class Derived, but its memory is not deallocated. Is it OK to access members strong_cnt and weak_cnt after that?

From implementation point, it should be OK, at least when there is no virtual inheritance involved. Because Derived* can be statically cast to RefCounters* (adding compile-time constant offset to address), and the memory of RefCounters should not be touched by destructor of Derived class.

Here is a code sample:

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";

Is such code considered undefined behavior by C++ standard? Is there any practical reason why it can fail to work? Can it be made legal by minor changes or adding some constraints?

P.S. Supposedly, such code sample allows to make intrusive_ptr + weak_ptr combo, such that weak_ptr can be always obtained from an object pointer if at least one weak_ptr is still pointing at it. More details in this question.

Community
  • 1
  • 1
stgatilov
  • 5,333
  • 31
  • 54
  • On an unrelated note, why doesn't `RefCounters` handle its counters itself? Meaning the destructor *would* do something (i.e. decrease one or both counters)? – Some programmer dude Dec 25 '16 at 06:30
  • 1
    Since `RefCounters` has a trivial destructor, its lifetime ends when its storage is reused or released, per **[basic.life]/1**. An explicit destructor call is a no-op and shouldn't affect anything; nor should the fact that a particular instance of `RefCounters` is a subobject of a larger object. – Igor Tandetnik Dec 25 '16 at 06:32
  • @IgorTandetnik: Thank you for your comment. I guess it should make legal the first case (i.e. with no derived classes). But I'm still not sure about the second case with derived classes. – stgatilov Dec 25 '16 at 06:35
  • 1
    So, `RefCounters` object should still be alive after `pDer->~Derived();` - but accessing it via `pDer` exhibits undefined behavior, I believe. Something like this should work though: `RefCounters* pRef = pDer; pDer->~Derived(); pRef->strong_cnt++;` – Igor Tandetnik Dec 25 '16 at 06:41
  • 1
    See also: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1285 – Igor Tandetnik Dec 25 '16 at 06:49
  • @IgorTandetnik: You link is really great! So it seems that standard writers had some hard time trying to save the behavior of C structs and to disallow working with destroyed object. And what about using `static_cast(pDer)`? Or implementing it manually by adding compile-time constant to pointer address? – stgatilov Dec 25 '16 at 07:03
  • @IgorTandetnik I do not know of any implementation that treats trivially destructible subobjects as living past the lifetime of their complete object; since objects are ultimately formed from the built-in types (all of which are trivially destructible, obviously), such a reading would disallow a multitude of optimizations. – T.C. Dec 25 '16 at 10:49
  • @T.C. I do, too, have my doubts - more of them since I found DR1285. However, could you provide an example of an optimization that would be precluded by such an interpretation? I couldn't think of one. It would seem that the scenario in question - explicitly running the destructor while leaving the storage intact - would be pretty rare and uncommon. Though I admit it's entirely possible - likely even - that I'm missing something obvious. – Igor Tandetnik Dec 25 '16 at 15:19
  • 1
    @stgatilov `static_cast(pDer)` is explicitly prohibited by **[basic.life]/(5.4)**: "Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways... The program has undefined behavior if:... the pointer is used as the operand of a `static_cast`..." – Igor Tandetnik Dec 25 '16 at 15:24
  • 1
    @stgatilov However, `RefCounters* pRef = pDer;` should be valid, I think. **[basic.life]/(5.3)** says: "...The program has undefined behavior if:... the pointer is implicitly converted (4.10) to a pointer to a virtual base class..." Which would appear to suggest that an implicit conversion to a pointer to a non-virtual base class is OK. In fact, the word `virtual` was added to this clause by the resolution for [DR597](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#597), in order to allow just such an implicit conversion, as far as I can tell. – Igor Tandetnik Dec 25 '16 at 15:27
  • @IgorTandetnik To start with, it means that you can't optimize away dead writes to members in destructors. – T.C. Dec 25 '16 at 21:20
  • @T.C. Neither [clang](http://rextester.com/HEYUV16681) nor [MSVC](http://rextester.com/MSKO46432) appear to perform the optimization you describe. Only [gcc](http://rextester.com/KYSQM60679) does (and I'm not 100% sure it's correct in doing so). – Igor Tandetnik Dec 26 '16 at 01:56

2 Answers2

0

I believe that your approach is bad. There is a nice link in comments that shows debate about the details of the standard. Once there is a debate there is good chance that different compilers will implement this detail differently. Even more. The same compiler may change its implementation from one version to another.

The more you use various dark corners, the bigger is the chance that you will meet with problems.

Bottom line. What are willing to achieve? Why can't you do this using ordinary C++ language features?

Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51
  • Just read the "P.S." section of the question, I have added a link there. But please do not post things like "you should not do that", because this is not a good answer. – stgatilov Dec 25 '16 at 07:30
  • @stgatilov, It looks that you need to rework your question because the questions "is base class (and its **primitive** data members) still alive after destroying derived class" is pretty unrelated to discusstion of the ways to design smart pointers. – Kirill Kobelev Dec 25 '16 at 07:36
  • You ask "if this code is legal?", "can it be modified to become legal"? My point is that since there is debate even in the committee about this, the doubt should be shifted into direction "illegal". The code is illegal at least until the debate will be resolved and compilers will be aligned to this resolution. – Kirill Kobelev Dec 25 '16 at 07:43
  • You are absolutely right that the discussion of smart pointers is completely off-topic in this question. If you are interested in it, just go to the linked question, it is exactly about it. So there is no need to rework this question in that direction. – stgatilov Dec 25 '16 at 13:35
  • It makes sense to rework it to prompt people to remove their downvotes. – Kirill Kobelev Dec 25 '16 at 13:44
0

This means that if an object of this type is destroyed with explicit call of destructor (but its memory is not deallocated), then we would be able to work with its members normally after the object is dead.

This premise is incorrect, which disposes with the rest of your question as well; it certainly will not be well-defined when those members are accessed through a pointer to a derived class type, either.

To understand why your premise is incorrect, let's look at the C++14 standard (the one that was in effect at the time the question was asked). According to [class.cdtor]/3:

[...] To form a pointer to (or access the value of) a direct non-static member of an object obj, the construction of obj shall have started and its destruction shall not have completed, otherwise the computation of the pointer value (or accessing the member value) results in undefined behavior.

The fact that the destructor of RefCounters is trivial does not provide an exception to this rule. However, it does provide an exception to [class.cdtor]/1:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior. For an object with a non-trivial destructor, referring to any non-static member or base class of the object after the destructor finishes execution results in undefined behavior.

For a destroyed RefCounters object, whose memory has not been released or reused yet, it is well-defined to merely refer to the strong_cnt or weak_cnt member, but it is undefined to form a pointer to the member or to attempt to access its value. This is also mentioned in [basic.life]/5:

[...] or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. [...] Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

  • [...]
  • the pointer is used to access a non-static data member or call a non-static member function of the object, or
  • [...]

Note that in C++20, it was clarified that even destruction of a scalar type ends its lifetime. So when you destroy a RefCounters object, this also destroys its size_t members, and they may no longer be accessed. This gives an additional reason why your code has UB.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312