1

I got a small problem which i don't understand for now and can't find explanation for it. I read about how to use std::unique_ptr in PIMPL idiom and it works but.. not in one weird situation, which of course occured to me now.

The simplest - i will show a simplified code example which reproduce a problem (Compiling with VS2017 Community).

header.h ##

Forward declaration of Forward class, and template class TestForward which has virtual function returning unique_ptr.

class Forward;
using TestUniquePtr = std::unique_ptr<Forward>;
TestUniquePtr make_ptr();

template<int a>
class TestForward {

public:
    virtual TestUniquePtr foo();
};

template<int a>
TestUniquePtr TestForward<a>::foo() {
    return make_ptr();
}

forward.h

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

class Forward {
public:
    ~Forward() {
        std::cout << "HAAA" << std::endl;
    }
};

forward.cpp

#include "forward.h"

TestUniquePtr make_ptr() {
    return TestUniquePtr{ new Forward };
}

main.cpp

File which does not compile due to 'can't delete an incomplete type'. Notice that function foo is not even called in here. So compiler should try to compile this function in this unit? If this function is not virtual or TestForward is not a template - it works.

#include "header.h"

int main (int argc, char *argv[]) {
    TestForward<3> a;
    return 0;
}

I know how can i fix this - by defining my deleter which is not a template, and write its definition in forward.cpp but.. I think this should work, so please help me find out why template+virtual make it not working :(

1
  • 2
    foo will be instantiated anyway to populate vtable. And type Forward must be complete at foo definition as it involves move and destruction of temporary returned by make_ptr. Commented Sep 29, 2017 at 8:06

1 Answer 1

3

There is so much going on here that all plays together into this error ...

First, consider this: the C++ standard says that if you do this:

struct Incomplete;
void foo(Incomplete* p) { delete p; }

this is legal, but if the full definition of Incomplete turns out to have a non-trivial destructor, the program has undefined behavior. I believe this solely for compatibility with early C-like C++ programs.

So, to improve the safety of programs, the default deleter of unique_ptr uses a "safe delete", i.e. one that fails to compile for incomplete types. This means that the instantiation of the unique_ptr destructor must be aware of the full definition of the pointed-to class.

In your program, any code that uses the TestUniquePtr destructor must therefore be aware of the full definition of Forward.

TestForward::foo uses the destructor. make_ptr returns an object. foo move-constructs its own return value from this object, and then destroys the source. (In the actual generated code, this is most likely optimized away by the return value optimization, but the code must still be valid without it.)

And where/why is TestForward<3>::foo used? Well, since it is virtual, it must be instantiated whereever the vtable of the class is instantiated. And since it is a template instantiation, the vtable is instantiated wherever the constructor is called (because the constructor needs to write the vtable pointer to the object). And the constructor is called in main.

If foo is not virtual, there's no need to instantiate it. And if TestForward is not a template, I guess you put foo into some separate source file instead of the header, so the error didn't manifest for main.


So how do you fix this?

In a typical Pimpl context, you fix this by tightly controlling who instantiates the destructor of the unique_ptr. You explicitly declare the destructor of the interface class and put the definition into the source file where the impl class definition is known.

If, however, you want to hand out unique_ptrs to your incomplete class as opaque handles, you need to replace the default deleter.

// header.h
class Forward;
struct ForwardDeleter {
  void operator ()(Forward* ptr);
};
using TestUniquePtr = std::unique_ptr<Forward, ForwardDeleter>;

// forward.cpp
class Forward { ... };
void ForwardDeleter::operator ()(Forward* ptr) { delete ptr; }
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.