sly
sly

Reputation: 1780

unique_ptr with type erased destructor does not quite work (works with warnings)

There is a nice little technique here to allow the use of std::unique_ptr with incomplete types.

Here is the relevant code:

// File: erasedptr.h
#include <memory>
#include <functional>

// type erased deletor (an implementation type using "veneer")
template <typename T>
struct ErasedDeleter : std::function<void(T*)>
{
    ErasedDeleter()
        : std::function<void(T*)>( [](T * p) {delete p;} )
    {}
};

// A unique_ptr typedef
template <typename T>
using ErasedPtr = std::unique_ptr<T, ErasedDeleter<T>>;


// Declare stuff with an incomplete type
struct Foo;
ErasedPtr<Foo> makeFoo();


// File: main.cpp (Foo's definition is not available in this translation unit)
#include "erasedptr.h"
int main() {
    ErasedPtr<Foo> f;  // [R1]
    f = makeFoo();
    // ~Foo() gets called fine
}

// File: foo.cpp
#include <iostream>
#include "erasedptr.h"
struct Foo {
    ~Foo() { std::cout << "~Foo()\n" ; }
};
ErasedPtr<Foo> makeFoo() { return ErasedPtr<Foo>(new Foo); }

This works on all compilers I tried: gcc 4.9, clang 3.5, and msvc VS13 and VS15. But they all generate the following warning:

 deletion of pointer to incomplete type 'Foo'; no destructor called

If [R1] above is replaced with ErasedPtr<Foo> f( makeFoo() );, the warning does not manifest.

At the end, the destructor does get called and there doesn't seem to be an actual problem. The warning is problematic, because it can not be ignored in quality-critical environments, and this otherwise very useful pattern is unavailable.

To reproduce, create the 3 files erasedptr.hpp, main.cpp, foo.cpp as above and compile.

So the question is: what is going on? Could there be any alternative implementation to circumvent this warning?

Upvotes: 6

Views: 2068

Answers (1)

klaus triendl
klaus triendl

Reputation: 1359

Your question is valid but the code you are trying to use is a bit convoluted, so let me haul off to get to the wanted solution.

  • What you do here isn't type erasure (and Andrzej is wrong about this, too) - you just capture the deletion into a runtime function value, without any benefit BTW. Type erasure OTH is when other code parts lose information about the initial type.
  • I tried your code on VS 2015, which also calls the destructor ~Foo(), however this is only big luck, which means the compiler is doing some fancy stuff. If you don't use std::function but write your own custom deleter then the destructor isn't called.

Solution 1 - type erasure

If you really would like to erase the type you would write/use ErasedPtr the following way:

erasedptr.h

// file erasedptr.h

#include <memory>
#include <functional>

// make type erased deleter
template <typename T>
std::function<void(void*)> makeErasedDeleter()
{
    return {
        [](void* p) {
            delete static_cast<T*>(p);
        }
    };
};

// A unique_ptr typedef
template <typename T>
using ErasedPtr = std::unique_ptr<T, std::function<void(void*)>>;

foo.cpp

// file foo.cpp

#include <iostream>
#include "erasedptr.h."

struct Foo {
    ~Foo() { std::cout << "~Foo()\n" ; }
};

// capture creation and deletion of Foo in this translation unit
ErasedPtr<Foo> makeFoo() {
    return { new Foo, makeErasedDeleter<Foo>() };
}

main.cpp

// file main.cpp (Foo's definition is not available in this translation unit)

#include "erasedptr.h"

// fwd decl Foo
struct Foo;
ErasedPtr<Foo> makeFoo();

int main() {
    ErasedPtr<Foo> f;  // [R1]
    f = makeFoo();
    // ~Foo() gets called fine
}

This way only foo.cpp needs to know about the actual type and it captures the deletion into std::function.

Solution 2 - incomplete types, really

What you actually really want is to deal with incomplete types. The 'issue' you have with STL's default deleter std::default_delete is that it asserts at compile time whether deletion is safe - and it is damn right about it!

To make it work correctly is to tell the compiler/linker you actually cared about a correct implementation of the deletion, using explicit template instantiation. This way you don't need any special typedef/template alias for your unique pointer:

foo.cpp

// file foo.cpp

#include "foo_fwddecl.h"

// capture creation of Foo in this translation unit
std::unique_ptr<Foo> makeFoo() {
    return std::make_unique<Foo>();
}

// explicitly instantiate deletion of Foo in this translation unit
template void std::default_delete<Foo>::operator()(Foo*) const noexcept;
template void std::default_delete<const Foo>::operator()(const Foo*) const noexcept;
// note: possibly instantiate for volatile/const volatile modifiers

foo_fwddecl.h

#include <memory>

struct Foo;

std::unique_ptr<Foo> makeFoo();

extern template void std::default_delete<Foo>::operator()(Foo*) const noexcept;
extern template void std::default_delete<const Foo>::operator()(const Foo*) const noexcept;
// note: possibly instantiate for volatile/const volatile modifiers

main.cpp

// file main.cpp (Foo's definition is not available in this translation unit)

#include "foo_fwddecl.h"

int main() {
    std::unique_ptr<Foo> f;  // [R1]
    f = makeFoo();
    // ~Foo() gets called fine
}

Upvotes: 4

Related Questions