Sneftel Sneftel - 6 days ago 6
C++ Question

Optionally safety-checked cast on possibly incomplete type

Pursuant to a simple, intrusively reference-counted object system, I have a

template<typename T> class Handle
, which is meant to be instantiated with a subclass of
CountedBase
.
Handle<T>
holds a pointer to a
T
, and its destructor calls
DecRef
(defined in
CountedBase
) on that pointer.

Normally, this would cause problems when trying to limit header dependencies by using forward declarations:

#include "Handle.h"

class Foo; // forward declaration

struct MyStruct {
Handle<Foo> foo; // This is okay, but...
};

void Bar() {
MyStruct ms;
} // ...there's an error here, as the implicit ~MyStruct calls
// Handle<Foo>::~Handle(), which wants Foo to be a complete
// type so it can call Foo::DecRef(). To solve this, I have
// to #include the definition of Foo.


As a solution, I rewrote
Handle<T>::~Handle()
as follows:

template<typename T>
Handle<T>::~Handle() {
reinterpret_cast<CountedBase*>(m_ptr)->DecRef();
}


Note that I'm using
reinterpret_cast
here instead of
static_cast
, since
reinterpret_cast
doesn't require the definition of
T
to be complete. Of course, it also won't perform pointer adjustment for me... but as long as I'm careful with layouts (
T
must have
CountedBase
as its leftmost ancestor, must not inherit from it virtually, and on a couple of unusual platforms, some extra vtable magic is necessary), it's safe.

What would be really nice, though, would be if I could get that extra layer of
static_cast
safety where possible. In practice, the definition of
T
is usually complete at the point where
Handle::~Handle
is instantiated, making it a perfect moment to double-check that
T
actually inherits from
CountedBase
. If it's incomplete, there's not much I can do... but if it's complete, a sanity check would be good.

Which brings us, finally, to my question:
Is there any way to do a compile-time check that
T
inherits from
CountedBase
which will not cause a (spurious) error when
T
is not complete?


[Usual disclaimer: I'm aware that there are potentially unsafe and/or UB aspects to the use of incomplete types in this way. Nevertheless, after a great deal of cross-platform testing and profiling, I've determined that this is the most practical approach given certain unique aspects of my use case. I'm interested in the compile-time checking question, not a general code review.]

Answer

Using SFINAE on sizeof to check whether the type is complete :

struct CountedBase {
    void decRef() {}
};

struct Incomplete;
struct Complete : CountedBase {};

template <std::size_t> struct size_tag;

template <class T>
void decRef(T *ptr, size_tag<sizeof(T)>*) {
    std::cout << "static\n";
    static_cast<CountedBase*>(ptr)->decRef();
}

template <class T>
void decRef(T *ptr, ...) {
    std::cout << "reinterpret\n";
    reinterpret_cast<CountedBase*>(ptr)->decRef();
}

template <class T>
struct Handle {
    ~Handle() {
        decRef(m_ptr, nullptr);
    }

    T *m_ptr = nullptr;
};

int main() {
    Handle<Incomplete> h1;
    Handle<Complete> h2;
}

Output (note that the order of destruction is reversed) :

static
reinterpret

Live on Coliru

Trying it with a complete type that does not derive from CountedBase yields :

main.cpp:16:5: error: static_cast from 'Oops *' to 'CountedBase *' is not allowed

That being said, I think a more elegant (and more explicit) approach would be to introduce a class template incomplete<T>, such that Handle<incomplete<Foo>> compiles to the reinterpret_cast, and anything else tries to static_cast.

Comments