4

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?

sly
  • 1,608
  • 1
  • 10
  • 13
  • I think this will only work if `makeFoo` can see the definition of `Foo` so that `ErasedDeleter` can call the destructor correctly. – Bryan Chen Mar 11 '16 at 00:48
  • `makeFoo`'s defition does see the definition of `Foo` (obviously, because it is `new`ed as well). As I indicated, it works fine in that respect. The issue is a default constructed `ErasedPtr` seems to be causing this bogus(?) warning. – sly Mar 11 '16 at 00:51
  • i am almost inclined to think the compilers are correct, and this pattern is simply unsafe... – sly Mar 11 '16 at 01:21
  • Try using a `virtual` destructor. My guess is that might make the problems more obvious. But I think this might be a duplicate of [Delete objects of incomplete type](http://stackoverflow.com/q/4325154/1287251) or [Why, really, deleting an incomplete type is undefined behaviour?](http://stackoverflow.com/q/2517245/1287251) From my understanding it's undefined behavior if `Foo` has a nontrivial destructor (which it does, in this case). – Cornstalks Mar 11 '16 at 19:44
  • this is a unique_ptr essentially, whether or not the object it maintains has a virtual destructor is irrelevant (it could be a non-class for all it cares). Deleting an incomplete type is of course undefined, because the destructor isn't called (memory is released, but who knows what other resource the object was holding onto). The question is not about whether or not deleting incomplete type is problematic. The question is why it is happening in *this particular pattern*... – sly Mar 11 '16 at 20:04
  • My point with trying a `virtual` destructor was that if this really is a problem for the compiler, then a non-trivial `virtual` destructor (preferably with some inheritance) seems like a good way to test if you can get it to blow up. It won't necessarily prove the method is safe, but it might prove it is unsafe. – Cornstalks Mar 11 '16 at 20:30
  • Also, the reason I think this might be a duplicate of those other questions is because it's pretty clear what's going on: `ErasedDeleter` is trying to `delete p` despite `Foo` being an incomplete type (in main.cpp, at least). But I haven't voted to close your question as a duplicate because I think you can slightly modify/clarify your questions so it's clearer it's not a duplicate (i.e. "Why does `ErasedPtr f(makeFoo());` not result in a warning?" to which the answer might be "because no diagnostic is required for this UB behavior"). – Cornstalks Mar 11 '16 at 20:41
  • `f(makeFoo())` does **not** result in undefined behavior, neither is the code as I have written (R1) version. You do not need virtual destructors etc to observe the behavior: The `cout` in the destructor is there for this purpose; so if you actually attempt to reproduce you'll either see or not see the destructor output, which would indicate complete or incomplete deletion, respectively. – sly Mar 11 '16 at 20:50
  • The original code does not call `~Foo()`, at least on my computer, and with the multiple compilers I have on my machine. This is undefined behavior. That it "works" as expected in the alternate form (`ErasedPtr f(makeFoo() );`) -- that's pure luck. There's no telling what a compiler will do when you invoke undefined behavior. Sometimes, it might be nice enough and warn you about it. But that, too, is pure luck. – David Hammen Mar 11 '16 at 21:04
  • @David the undefined behavior you seem to be referencing here is "deletion of an incomplete type". However, with `ErasedPtr f(makeFoo())` (and [R1] with `f` getting reassigned), this undefined behavior is *not supposed to be* manifested. There could be another undefined behavior, and it could be the answer to my question, but the deletion itself is *clearly* not undefined (`std::function` is the Deletor of unique ptr, and it calls a type erased function, which when instantiated and constructed (in `foo.cpp`) contains the *full type*). – sly Mar 11 '16 at 21:17
  • @DavidHammen Which particular compiler and versions have you tried this? – sly Mar 11 '16 at 21:21
  • I used g++4.8, g++5, clang++3.7, and clang++3.8, all on OSX. I have more compilers. All tested ones exhibited the same behavior: With the as-asked implementation, they all issued a warning about deleting an incomplete type, and no call to `~Foo()` was made. With the as-suggested implementation, no warning, and a call to `~Foo()`. – David Hammen Mar 11 '16 at 21:29
  • Thanks, I'll try these compilers also. So there is the question then: Why does this lead to undefined behavior (please try to reference what does or doesn't happen with regards to type erasure via `std::function` as the Deletor)? – sly Mar 11 '16 at 21:37
  • I tried clang++ 3.7 on Debian (Linux) and darwin14 (MacOS), I can not reproduce what you are reporting. The compiler *does not* generate a warning in either version, and `~Foo()` gets called. – sly Mar 11 '16 at 21:56
  • `~Foo()` does not get called for me with your code when using clang from Xcode 7.2.1. – Cornstalks Mar 12 '16 at 00:00

1 Answers1

3

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
}
klaus triendl
  • 1,096
  • 11
  • 23