Search code examples
c++undefined-behaviorunique-ptr

Is a unique_ptr with custom deleter never invoked when initialized with nullptr


In scenarios where you interface with C libraries which manage the creation/deletion of pointers, I saw a recent buggy code where a struct was managed by a unique_ptr before the pointer was actually pointing to a valid memory location, so the raw ptr was nullptr before it was passed on to a C API. An example in code to simulate this:

#include <memory>
#include <iostream>
struct Foo {
  std::string s;
};
// A simple fn to simulate C libararies which use a outptr
void init_foo(Foo** foo_ptr_ptr) {
    std::cout << "Initializing Foo\n"; 
    Foo* f = new Foo; 
    *foo_ptr_ptr = f;
};
// custom resource deletion via the C API
void delete_foo(Foo* foo) {
    std::cout << "Invoking destructor!\n";
  if (foo != nullptr) {
    std::cout << "Deleting Foo\n";
    delete foo;
  } 
}

int main() {
    Foo * foo {nullptr};
    // Ideally you should init_foo(&foo) here before creating a uniq_ptr
    std::unique_ptr<Foo, decltype(&delete_foo)> foo_ptr {foo, &delete_foo};
    init_foo(&foo); // Buggy initialization, however delete_foo will never be invoked!
    return 0;
}

In case of a buggy initialization ie. you create the unique_ptr with ptr constructor before the ptr was actually valid, and you initialize it later what actually happens, is the destructor never invoked because it is UB to change the memory location of unique_ptr's managed raw ptr?

For eg. in the above code where init_foo was called after unique_ptr was created, the destructor of the unique_ptr was never invoked. Is this because this is a Undefined behaviour?

Of course calling the unique_ptr after init_foo will have the expected behaviour where the construction and the expected destructor is called.


Solution

  • [...] what actually happens, is the destructor never invoked because it is UB to change the memory location of unique_ptr's managed raw ptr?

    You never change the pointer managed by the std::unique_ptr to anything other than null and that's why the delete_foo is never invoked, not even with null as parameter. There's no undefined behaviour happending here, it's just that the code posted in the question doesn't behave the way you expect it to.

    std::unique_ptr contains a member variable holding the pointer and the constructor you're using in the question takes the pointer by value, i.e. it copies the current value of foo resulting in later changes of the value of foo in having no effect on foo_ptr.

    You could either call init_foo(&foo) before calling the constructor or provide the std::unique_ptr with ownership of the object later using std::unique_ptr::reset:

    Foo* foo{ nullptr };
    // Ideally you should init_foo(&foo) here before creating a uniq_ptr
    std::unique_ptr<Foo, decltype(&delete_foo)> foo_ptr{ foo, &delete_foo };
    init_foo(&foo); // Buggy initialization, however delete_foo will never be invoked!
    foo_ptr.reset(foo);