Can I workaround unique_ptr<MyType> to not need MyType destructor definition when only storing nullptr?

Viewed 208

I need to use VS2012 compiler and have:

virtual std::unique_ptr<MyType> pass_through(std::unique_ptr<MyType> instance) override { return std::unique_ptr<MyType>(nullptr); };

That definition exists only for a project as a stub and without the MyType destructor, I get following error:

error LNK2001: unresolved external symbol "public: __thiscall MyType::~MyType(void)" (??1MyType@@QAE@XZ)

So I created a definition:

MyType::~MyType() {}

and that is the problem, I don't want to have the confusing definition just so the function above passes build.. so is there a way to not need to specify the destructor definition and still have valid implementation of that pass_through method?

Maybe I can somehow change the signature of the method or it's logic to do basically the same in the primary implementation, it does something like:

std::unique_ptr<MyType> pass_through(std::unique_ptr<MyType> instance)
{
    if (!instance) {
        instance= std::unique_ptr<MyType>(new MyType(/*arguments*/));
    }
    instance->something();
    return instance;
}

Btw I see similar question are downvoted/closed, but still in the suggestions I don't see any relevant answer and I also used google before and still no hit => maybe somehow promote the relevant question with good answer, if there is any?

3 Answers

I think there are few aspects to a question. First is the understanding of what the unique_ptr does:

It will wrap your class in a RAII fashion holding pointer to your class and release it when being destructed or reasigned. To make sure we are on the same page, lets explore the examplary implementation of the unique_ptr (taken from this question):

template<typename T>
class unique_ptr {
private:
    T* _ptr;
public:
    unique_ptr(T& t) {
       _ptr = &t;
    }
    unique_ptr(unique_ptr<T>&& uptr) {
       _ptr = std::move(uptr._ptr);
       uptr._ptr = nullptr;
    }
    ~unique_ptr() {
       delete _ptr;
    }
    unique_ptr<T>& operator=(unique_ptr<T>&& uptr) {
       if (this == uptr) return *this;
       _ptr = std::move(uptr._ptr);
       uptr._ptr = nullptr;
       return *this;
    }

    unique_ptr(const unique_ptr<T>& uptr) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& uptr) = delete;
};

As you can see the unique pointer destructor calls delete on the actual object in ~unique_ptr() function.

Now lets have a look at the standard:

3.7.4 Dynamic storage duration [basic.stc.dynamic] 1 Objects can be created dynamically during program execution (1.9), using new-expressions (5.3.4), and destroyed using delete-expressions (5.3.5). A C++ implementation provides access to, and management of, dynamic storage via the global allocation functions operator new and operator new[] and the global deallocation functions operator delete and operator delete[].

Also:

If the value of the operand of the delete-expression is not a null pointer value, the delete-expression will invoke the destructor (if any) for the object or the elements of the array being deleted. In the case of an array, the elements will be destroyed in order of decreasing address (that is, in reverse order of the completion of their constructor; see 12.6.2).

Now given this is the behaviour enforced by standard, you need to have a destructor declared and defined.

The definition, even if empty, needs to tell linker where to jump (or tha it can simply do nothing) in case of object being deleted.

So in conclusion, you need to either declare and define the destructor yourself, relly on automatically created default one (not even declare it) or declare it marking it as default. (say the compiler that you do define it but it should have a default implementaion).

You don't need the definition in the same compilation unit. I think you're linking wrong. (The .cpp file which contains void MyType::something() { /* ... */ } should also have MyType::~MyType() { /* ... */ }, so if you use (MyType*)->something, you should be able to use the destructor).

If this is indeed what you want to do (Compile a small section of your program without linking the unit with the destructor, for whatever reason), it would be impossible with the default std::unique_ptr<T>, which needs to call delete (T*);, which will ultimately need the destructor to be available.

You can use a custom deleter, which you can special case a "do nothing" deleter. This means that you don't need a MyType::~MyType symbol:

#include <memory>

class MyType {
public:
    void something();

    ~MyType();
};

// `MyUniquePtr` calls a stored function pointer to delete it's value
using MyUniquePtr = std::unique_ptr<MyType, void(*)(const MyType*) noexcept>;

void MyTypeDeleter(const MyType* p) noexcept;

// This is compatible with `MyUniquePtr`'s deleter, and does nothing when called
void MyTypeNullDeleter(const MyType* p) noexcept {
    // For extra safety add this check
    if (p != nullptr) std::terminate();

    // Otherwise do nothing
    // (Don't need to delete a nullptr, don't need a destructor symbol)
}

MyUniquePtr pass_through(MyUniquePtr instance) {
    return MyUniquePtr(nullptr, MyTypeNullDeleter);
};
// In a seperate unit where the destructor is defined, you still
// have to use the `MyUniquePtr` type for the virtual functions,
// but need a deleter which actually deletes the pointer.
// That's what `MyTypeDeleter` is.

MyType::~MyType() { }

void MyTypeDeleter(const MyType* p) noexcept {
    delete p;
}

MyUniquePtr pass_through2(MyUniquePtr instance) {
    if (!instance) {
        instance = MyUniquePtr(new MyType(/*arguments*/), MyTypeDeleter);
    }
    instance->something();
    return instance;
}

But it's easy to accidentally have the "do nothing" deleter assigned to a non-null pointer.

I can think of only a single scenario where your problem may occur. And that is when you declare a destructor but do not define it:

struct X { ~X(); };

int main() {
  std::unique_ptr<X> p(new X); 
}

This trigger a linker error. So why don't you just simply omit the destructor declaration at all?

struct X { };

Now, everything works fine.


Note that if the destructor was deleted (either manually or by a compiler, e.g., by deriving from a non-destructible base), then, you would get a compile-time error, not the link-time one:

struct X { ~X() = delete; };
Related