Search code examples
c++unique-ptrmove-semantics

How to move a unique_ptr without custom deleter to another unique_ptr with custom deleter?


#include <memory>
#include <functional>
#include <iostream>

struct TestStruct {
    int a = 100;
};

struct StructDeleter {
    void operator()(TestStruct *ptr) const {
        delete ptr;
    }
};

std::unique_ptr<TestStruct, StructDeleter> MakeNewStruct() {
    std::unique_ptr<TestStruct> st(new TestStruct());

    std::unique_ptr<TestStruct, StructDeleter> customDeleterSt(std::move(st));
    std::cout << customDeleterSt->a << std::endl;
    return customDeleterSt;
}

int main() {
    auto a = MakeNewStruct();
    std::cout << a->a << std::endl;
    return 0;
}

The above code can not be compiled, how to move st to customDeleterSt? I get a st unique_ptr from the st creation interface, and I use the custom deleter to hide the implementation of st from my users, so how to move a unique_ptr without custom deleter to a unique_tr with custom deleter?

Thanks for any help!


Solution

  • As noted in the comments, the brute-force way is to have the source .release() to the constructor of the destination. However there is a much more elegant solution (imho):

    Add an implicit conversion from std::default_delete<TestStruct> to StructDeleter:

    struct StructDeleter {
        StructDeleter(std::default_delete<TestStruct>) {}  // Add this
    
        void operator()(TestStruct *ptr) const {
            delete ptr;
        }
    };
    

    Now the code works with the existing move construction syntax.

    Whether this is done via the converting constructor, or .release(), if StructDeleter can't (for whatever reasons) properly delete a pointer that std::default_delete handles, this will result in undefined behavior. It is only because StructDeleter calls delete ptr that either of these techniques works.

    As written, TestStruct does not need to be a complete type at the point of delete. However should TestStruct acquire a non-trivial destructor, you will also need to ensure that for any code that calls StructDeleter::operator()(TestStruct*), that TestStruct is a complete type, otherwise you're back into UB territory again. This assurance is one of the things that std::default_delete<TestStruct> does for you and that StructDeleter (as written) does not.

    If it is intended to keep ~TestStruct() trivial, it would be a good idea to add:

    static_assert(std::is_trivially_destructible<TestStruct>::value);