Does std::unique_ptr set its underlying pointer to nullptr inside its destructor?
Asked Answered
O

3

5

When implementing my own unique_ptr( just for fun), I found it cannot pass this test file from libstdcxx:

struct A;

struct B
{
  std::unique_ptr<A> a;
};

struct A
{
  B* b;
  ~A() { VERIFY(b->a != nullptr); }
};

void test01()
{
  B b;
  b.a.reset(new A);
  b.a->b = &b;
}

gcc passes this test file happily (of course, this file is from libstdcxx), while clang fails for the VERIFY part.

Question:

  1. Is it implementation dependent or undefined behavior?
  2. I guess this postcondition (b->a != nullptr) is important for gcc, otherwise it'll not have a test file for it, but I don't know what's behind it. Is it related to optimization? I know many UB are for better optimizations.
Ocana answered 17/1, 2019 at 13:36 Comment(6)
It would be nice to see your unique_ptr implementation for reference (at least the destructor).Assagai
@Assagai I think the behavior OP is talking about is from the standard unique_ptr (see the Wandbox links).Edin
I can't seem to reproduce this behavior anywhere. It passes on coliru.Nonanonage
Yes, the question is about std::unique_ptr, not my own oneOcana
@MárioFeroldi clang on coliru uses libstdc++ from GCC. Add -stdlib=libc++ and you will get an assertion failure.Edin
It seems libc++ clears the pointer? The standard doesn't seem to say anything about it here.Nonanonage
E
8

clang (libc++) seems to be non-compliant on this point because the standard says:

[unique.ptr.single.dtor]

~unique_ptr();
  1. Requires: The expression get_­deleter()(get()) shall be well-formed, shall have well-defined behavior, and shall not throw exceptions. [ Note: The use of default_­delete requires T to be a complete type. — end note  ]

  2. Effects: If get() == nullptr there are no effects. Otherwise get_­deleter()(get()).

So the destructor should be equivalent to get_deleter()(get()), which would imply that b->a cannot be nullptr within the destructor of A (which is called inside get_deleter() by the delete instruction).


On a side note, both clang (libc++) and gcc (libstdc++) sets the pointer to nullptr when destroying a std::unique_ptr, but here is gcc destructor:

auto& __ptr = _M_t._M_ptr();
if (__ptr != nullptr)
    get_deleter()(__ptr);
__ptr = pointer();

...and here is clang (call to reset()):

pointer __tmp = __ptr_.first();
__ptr_.first() = pointer();
if (__tmp)
   __ptr_.second()(__tmp);

As you can see, gcc first deletes then assigns to nullptr (pointer()) while clang first assigns to nullptr (pointer()) then delete1.


1 pointer is an alias corresponding to Deleter::pointer, if it exists, or simply T*.

Edin answered 17/1, 2019 at 14:14 Comment(7)
off-topic: pointer just defaults to Deleter::pointer or falls back to T * otherwise.Nonanonage
@Assagai I think you're right. The standard does not specify the post-condition but the behaviour between get_deleter()(get()) and clang's behavior is inconsistent. I'll update the answer.Edin
The VERIFY function tries to access an instance of a unique_ptr after the destructor of the unique_ptr has been started. Isn't that undefined behavior?Multicolor
@Multicolor This is not UB (AFAIK), you can call member functions (careful with virtual functions) and access members of an object being destroyed. See e.g. #10979750.Edin
I have to return to the topic. From N4659: >>20.5.4.10 Library object access 2 If an object of a standard library type is accessed, and the beginning of the object’s lifetime (6.8) does not happen before the access, or the access does not happen before the end of the object’s lifetime, the behavior is undefined unless otherwise specified.<< and >>6.8 Object lifetime (1.2) ... The lifetime of an object of type T ends when: ... T is a class type with a non-trivial destructor (15.4), the destructor call starts,<<Multicolor
@Multicolor eel.is/c++draft/class.cdtor#4 says that member functions can be called during destruction, and eel.is/c++draft/basic.life#4 indicates that rules for accessing object within lifetime and within construction/destruction are differents. Related questions I found on SO (including the one I linked previously) seems to say this is well-defined. Still, I agree that your quote may indicate otherwise... I am not expert enough regarding the standard to add more to this.Edin
Again, thanks for the answer. I am still not sure since one has to distinguish between the language requirements and the library requirements. You can certainly access member functions of your own classes during construction/destruction. But I am still not sure what the guarantees are for library classes.Multicolor
C
4

Both libstdc++ and libc++ are conforming because this is unobservable by a well-defined program. During the destructor's execution, [res.on.objects]/2 prohibits any attempt to observe (or modify, for that matter) the state of the unique_ptr on pain of undefined behavior:

If an object of a standard library type is accessed, and the beginning of the object's lifetime does not happen before the access, or the access does not happen before the end of the object's lifetime, the behavior is undefined unless otherwise specified.

In fact, unique_ptr's destructor is why this paragraph was added in the first place (by LWG2224).

Additionally, after the destruction is complete, the contents of the storage it occupies is indeterminate by [basic.life]/4:

The properties ascribed to objects and references throughout this document apply for a given object or reference only during its lifetime.

Cloudlet answered 20/2, 2021 at 18:29 Comment(0)
B
0

There is no requirement on the final state of the memory occupied by the std::unique_ptr<> after destruction. It wouldn't make sense to set it to null as the memory is being returned to where ever it was allocated from. GCC probably checks that its not null to make sure nobody added unnecessary code to clear it. Under the right circumstances, forcing a clear of the value when not needed could cause a performance hit.

Birefringence answered 17/1, 2019 at 15:10 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.