1

I have a situation in which an allocator for a container concurrently accessed needs to call both placement new and the destructor.

    template< class U > void destroy(U* p){
            p->~U();
    }

as it is I can end up calling the destruction repeatedly. This brought me to think whether something like this should be OK.

   std::vector<int>* p = (std::vector<int>*)malloc(sizeof(std::vector<int>));
   ::new (*p) std::vector<int>(30);
   (*p)[10] = 5;
   p->~std::vector<int>();
   p->~std::vector<int>();
   free(p);

I think this will work as long as the destruction of std::vector sets the data pointer to null or size to zero and when called again there is not a double free.

So, should classes be made such that accidental (or benign) double destruction should be equivalent to a single destruction?

In other words, should destruction be for example an idempotent operation?

(Note for simplicity that the destructor is not virtual in this example)

I find the question is similar to the problem of how to allow a moved class to be destructed later.


Some answers put runtime cost to make the case against supporting double destruction. There are costs, but they are similar to the cost of moving objects. In other words, if it is cheap to move, it is cheap to allow double destruction (if DD is not UB in the first place for other reasons, such as the standard).

To be specific:

class dummyvector{
   int* ptr;
   public:
   dummyvector() : ptr(new int[10]){}
   dummyvector(dummyvector const& other) = delete; // for simplicity
   dummyvector(dummyvector&& other) : ptr(other.ptr){
      other.ptr = nullptr; // this makes the moved object unusable (e.g. set) but still destructable 
   }
   dummyvector& operator=(dummyvector const&) = delete; // for simplicity
   void set(int val){for(int i = 0; i != 10; ++i) ptr[i]=val;}
   int sum(){int ret = 0; for(int i = 0; i != 10; ++i) ret += ptr[i]; return ret;}
   ~dummyvector(){
      delete[] ptr;
      ptr = nullptr; // this is the only extra cost for allowing double free (if it was not UB)
      // ^^ this line commented would be fine in general but not for double free (not even in principle)
   }
};
alfC
  • 14,261
  • 4
  • 67
  • 118
  • 1
    The way you describe it, you'd also want it to be resilient to unsequenced execution from different threads. Imo it makes no sense to protect against a particular UB implementation that may or may not work across compilers/optimization levels. Just use a proper lock-free container or use locks. – krzaq Jan 31 '18 at 03:27
  • 6
    Double destruction is undefined behavior, so the issue is moot. [basic.life] says that one of the things that ends the lifetime of an object is explicitly invoking the destructor, and "after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways." Calling a non-static method is listed as disallowed and results in undefined behavior. – Raymond Chen Jan 31 '18 at 03:42
  • @krzaq thank you. The question is mostly about double free. Even if I lock I could double free still. Honestly I don't know who should own the lock. The container? The allocator? Someone else above them both? The double free is one aspect of my problem. – alfC Jan 31 '18 at 03:46
  • The good news is that there are plenty of code analysis tools available to help detect potential instances of use-after-free. If there is a high likelihood of this occurring, you need to take a step back and see if there is a better design pattern that fits. – Justin Randall Jan 31 '18 at 03:51
  • @RaymondChen thank for the authoritative answer. I guess now I wonder if redestruction *is* one of the "limited ways" in which the pointer can be used. – alfC Jan 31 '18 at 04:12
  • 1
    @alfC: His comment explicitly says the opposite. "Calling a non-static method is listed as disallowed and results in undefined behavior." The destructor is itself a non-static method, and therefore cannot be called on an object that has already called it. – ShadowRanger Jan 31 '18 at 04:32
  • There is no benign double destruction. It is always undefined behavior, indicative of a programming error. – Jive Dadson Jan 31 '18 at 06:37

3 Answers3

6

Given that destruction implies the end of the object's lifetime, handling double-destruction is paying a cost (the additional work to leave the memory in a redestructable state) for a benefit no well-formed code should ever encounter.

Saying double-destruction is okay is equivalent to saying use-after-free is fine; sure, an allocator that allows it in specific circumstances might keep buggy code working, but that doesn't mean it's a feature worth preserving in the allocator if it prevents the allocator from working efficiently and correctly in non-pathological cases.

If you're ever in a position where double destruction is a possibility, you're probably in a position where use-after-destruction is possible, and now you've got to make every operation on your class check for and "handle" (whatever that means) the possibility of being called on a destroyed instance. You've compromised normal operations to allow for misuse, and that's a terrible road to start down.

Short version: Don't protect against double-destruction; the moment you're double destroyed, the code in question was entering undefined behavior territory anyway, and it's not your job to handle it. Write code that doesn't do terrible things in the first place.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Are you of the same opinion about self assignment, i.e. `this != &rhs` check? I think the principle remains the same. Don't cater to bad code. – DeiDei Jan 31 '18 at 03:41
  • 1
    @DeiDei: No, that's different. Self-assignment only applies to `operator=`. Use-after-destruction could be provoked by *any* operation on a class. Deletion-after-destruction is just a subset of use-after-destruction. – Nicol Bolas Jan 31 '18 at 03:43
  • 1
    @DeiDei: A properly written assignment-operator does not check for self-assignment, proper handling of that comes from getting good exception-safety. – Deduplicator Jan 31 '18 at 03:53
  • Now I am thinking that this question is indirectly related to c++11 move. Should a *moved* object be destructable? Of course yes, should be usable? Not necessarily some think. I don't see a lot of difference with asking if a *destructed* object should be redestructable. – alfC Jan 31 '18 at 04:10
  • @alfC: A moved object should be rendered defunct as cheaply as possible. If that leaves it "usable", so be it, but using it for anything (besides reassigning it) would be undefined behavior, so again, making *any* additional effort to keep it usable is optimizing for the pathological case at the expense of the normal/correct usage cases. – ShadowRanger Jan 31 '18 at 04:15
  • @DeiDei: As others have noted, that's specific to `operator=`, and it's legal (if unusual) to assign a variable to itself, so you need to handle that (though [my favored approach doesn't require a self assignment check](https://stackoverflow.com/a/3279550/364696), because it optimizes for the common case at the expense of the uncommon self-assignment case). Any form of use-after-destruction is illegal/undefined behavior, so handling it is ridiculous. – ShadowRanger Jan 31 '18 at 04:19
  • @ShadowRanger well yes, but when moving the object one must make everything necessary for it to be destructable and pay that cost. I am basically asking if the same should apply to destruction. – alfC Jan 31 '18 at 04:21
  • @alfC: No, because after destruction, there is no reasonable use case that accesses the object again. If you think you have such a use case, you're wrong. [As Raymond Chen already pointed out](https://stackoverflow.com/questions/48533939/should-classes-be-resilient-to-double-destroy/48534033#comment84063687_48533939), most uses of a destroyed object (including all calls of non-static methods, e.g. the destructor) are already undefined behavior. The compiler is within its rights to shove random garbage in the "redestructible" state you left, rendering your effort pointless. – ShadowRanger Jan 31 '18 at 04:25
  • @alfC: From your question, it sounds like you've got a serious [XY problem](https://meta.stackexchange.com/q/66377/322040) going on. You aren't coordinating concurrent access properly/safely, and your response is to ask if you can do horribly unsafe things to fix the intrinsically unsafe behaviors you're inviting. The answer is almost surely: Even if you tried to make a redestructible object, the same concurrency issues that cause double destruction would also cause *interleaved* destruction, and your efforts to leave a redestructible object are for naught, it's just a new race condition. – ShadowRanger Jan 31 '18 at 04:29
  • @ShadowRanger I can lock guard the destruction to avoid interleaved destruction but the question remains. – alfC Jan 31 '18 at 04:41
  • @alfC: If you can lock guard the destruction, you can lock guard the decision to destruct in the first place (any such lock would have to exist outside the object after all, so it could guard the choice to destruct too). Either way, *there is no question*: Double destruction is illegal. The compiler can optimize out your efforts to make it safe (e.g. omitting a set to `NULL` because the value can never be legally read again). Nothing you can do will make it legal, so focus on fixing the problem that is making you think idempotent destruction is a solution (because it isn't). – ShadowRanger Jan 31 '18 at 04:49
2

Well, it's not actually possible to do that. Or at least, not without significant pain.

See, after the destructor of a class has finished executing, all of its subobjects stop existing. So the moment the second call of your destructor tries to access any of its subobjects, you invoke UB.

So if you're going to be double-destruction safe, you would need to store state outside of the class to be able to tell if a particular instance was destroyed. And since you can have any number of instances of a class, the only way to handle this is to have the constructor allocate some memory and register the this pointer with something, which the destructor can check with.

And this has to happen for every instance of every double-destruction safe subobject of that object. That's a huge amount of overhead, all to stop something that shouldn't be happening.


And as Raymond Chen pointed out, merely the act of invoking double-destruction on any non-trivially-destructible type is undefined behavior.

[basic.life]/1 tells us that an object with a non-trivial destructor has its lifetime ended when the destructor is called. And [basic.life]/6 tells us:

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 represents the address of the storage location where the object will be or was located may be used but only in limited ways. [...] 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

A destructor is a "non-static member function of the object". So in fact, it is impossible to make a C++ type double-destruction safe.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • I might be incorrect but I asked the question because I envisioned that for example std vector can be implemented easily (for example just by making data nullptr). In practice this is the same as the code needed to move the object. After all move leaves the object in a state in which it can be destructit. Why not ask the same to the destructor itself? – alfC Jan 31 '18 at 03:53
  • @alfC: "*for example just by making data nullptr*" That doesn't work, both for the second reason and the first. In the first case, the source of `data()` is a member subobject, and that member subobject *has been destroyed*. Accessing destroyed objects invoke UB. – Nicol Bolas Jan 31 '18 at 03:54
  • 3
    "for example just by making data nullptr" That's the easy part "in practice". The hard part is turning off the part of the optimizer that correctly removes it as a dead store. – T.C. Jan 31 '18 at 04:01
  • @NicolBolas this is a key point. The way I understand it is that calling the destructor doesn't actually free any member, it just calls it's destructors. In the case of vector, the data pointer is still there. – alfC Jan 31 '18 at 04:02
  • @T.C. ok, that I didn't know. Optimizations is something I didn't think about. – alfC Jan 31 '18 at 04:05
0

No, you should not try to protect against abusing the code. The power of C++ lets abuses like this be possible, but you have to trust (and document) that the intended use is obeyed.

Are we going to go around putting up 12-foot-high fences around all of our bridges to deter and protect jumpers (at high costs), or should we just use the efficient and normal guardrail and trust that everyone will adhere to the intended and reasonable use case?

Justin Randall
  • 2,243
  • 2
  • 16
  • 23
  • I understand, but this case arose because in principle if destroy is idempotent then I didn't need to use a lock, and it was cheaper to allow different processes or threads to do "the same thing" several times. – alfC Jan 31 '18 at 03:59