5
votes

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.
3
It would be nice to see your unique_ptr implementation for reference (at least the destructor).user673679
@user673679 I think the behavior OP is talking about is from the standard unique_ptr (see the Wandbox links).Holt
I can't seem to reproduce this behavior anywhere. It passes on coliru.Mário Feroldi
Yes, the question is about std::unique_ptr, not my own oneChen Li
@MárioFeroldi clang on coliru uses libstdc++ from GCC. Add -stdlib=libc++ and you will get an assertion failure.Holt

3 Answers

8
votes

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.


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

3
votes

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.

0
votes

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.