22

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.

Community
  • 1
  • 1
yeputons
  • 8,478
  • 34
  • 67
  • 1
    Maybe related: http://stackoverflow.com/q/8829548 – Kerrek SB Mar 04 '17 at 17:24
  • 1
    This seems like many questions in one. 2 in particular is a whole can of worms, and could involve [`std::launder`](http://stackoverflow.com/questions/39382501/what-is-the-purpose-of-stdlaunder) – Barry Mar 04 '17 at 17:31
  • [basic.life]. Read it, then read it again. – T.C. Mar 04 '17 at 18:01
  • It might be better to edit this question so it reads less like asking for opinions (I read "is it OK" as a code-style-related thing) and more like asking about validity (i.e. basically s/is it OK/is it valid/). I mean, after actually reading your question, it's clear you're asking about the latter, but being explicit always helps. – Nic Mar 10 '17 at 02:11

2 Answers2

14

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
8

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

Community
  • 1
  • 1
George Hilliard
  • 15,402
  • 9
  • 58
  • 96
  • Interesting point. In most cases, you could get around this by constructing first and then moving when using placement new. – chris Mar 04 '17 at 17:55
  • @chris I don't think so, because there's nothing stopping the move constructor from throwing just the same as my sample constructor. In either case, the original object is still going to get double-destroyed. – George Hilliard Mar 04 '17 at 17:57
  • 1
    It's strongly encouraged to have a `noexcept` move constructor. `std::vector` takes advantage of these to optimize its operations. Most classes should have a non-throwing move constructor, so that's why I said in most cases. – chris Mar 04 '17 at 18:03
  • @chris: But what are you going to move from? – Benjamin Lindley Mar 04 '17 at 18:10
  • Oh, nevermind, I see what you're saying. Construct an object before calling the destructor of the original, and then move from that. – Benjamin Lindley Mar 04 '17 at 18:13
  • Yeah. This reminds me of `variant`. Boost's double buffers and allocates to do so in order to have a strong exception guarantee on copy, whereas the standard one would be left in the "valueless by exception" state. There are definitely tradeoffs if this is for something important. – chris Mar 04 '17 at 18:37