Is it allowed to call destructor explicitly follow

2019-02-11 13:40发布

问题:

I know that calling destructor explicitly can lead to undefined behavior because of double destructor calling, like here:

#include <vector>

int main() {
  std::vector<int> foo(10);
  foo.~vector<int>();
  return 0;  // Oops, destructor will be called again on return, double-free.
}

But, what if we call placement new to "resurrect" the object?

#include <vector>

int main() {
  std::vector<int> foo(10);
  foo.~vector<int>();
  new (&foo) std::vector<int>(5);
  return 0;
}

More formally:

  1. What will happen in C++ (I'm interested in both C++03 and C++11, if there is a difference) if I explicitly call a destructor on some object which was not constructed with placement new in the first place (e.g. it's either local/global variable or was allocated with new) and then, before this object is destructed, call placement new on it to "restore" it?
  2. If it's ok, is it guaranteed that all non-const references to that object will also be ok, as long as I don't use them while the object is "dead"?
  3. If so, is it ok to use one of non-const references for placement new to resurrect the object?
  4. What about const references?

Example usecase (though this question is more about curiosity): I want to "re-assign" an object which does not have operator=.

I've seen this question which says that "overriding" object which has non-static const members is illegal. So, let's limit scope of this question to objects which do not have any const members.

回答1:

First, [basic.life]/8 clearly states that any pointers or references to the original foo shall refer to the new object you construct at foo in your case. In addition, the name foo will refer to the new object constructed there (also [basic.life]/8).

Second, you must ensure that there is an object of the original type the storage used for foo before exiting its scope; so if anything throws, you must catch it and terminate your program ([basic.life]/9).

Overall, this idea is often tempting, but almost always a horrible idea.

  • (8) If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

    • (8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and
    • (8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
    • (8.3) the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
    • (8.4) the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).
  • (9) If a program ends the lifetime of an object of type T with static (3.7.1), thread (3.7.2), or automatic (3.7.3) storage duration and if T has a non-trivial destructor, the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined. This is true even if the block is exited with an exception.

There are reasons to manually run destructors and do placement new. Something as simple as operator= is not one of them, unless you are writing your own variant/any/vector or similar type.

If you really, really want to reassign an object, find a std::optional implementation, and create/destroy objects using that; it is careful, and you almost certainly won't be careful enough.



回答2:

This is not a good idea, because you can still end up running the destructor twice if the constructor of the new object throws an exception. That is, the destructor will always run at the end of the scope, even if you leave the scope exceptionally.

Here is a sample program that exhibits this behavior (Ideone link):

#include <iostream>
#include <stdexcept>
using namespace std;

struct Foo
{
    Foo(bool should_throw) {
        if(should_throw)
            throw std::logic_error("Constructor failed");
        cout << "Constructed at " << this << endl;
    }
    ~Foo() {
        cout << "Destroyed at " << this << endl;
    }
};

void double_free_anyway()
{
    Foo f(false);
    f.~Foo();

    // This constructor will throw, so the object is not considered constructed.
    new (&f) Foo(true);

    // The compiler re-destroys the old value at the end of the scope.
}

int main() {
    try {
        double_free_anyway();
    } catch(std::logic_error& e) {
        cout << "Error: " << e.what();
    }
}

This prints:

Constructed at 0x7fff41ebf03f

Destroyed at 0x7fff41ebf03f

Destroyed at 0x7fff41ebf03f

Error: Constructor failed