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.]
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With