Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.]

like image 527
Sneftel Avatar asked Oct 30 '22 22:10

Sneftel


1 Answers

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.

like image 173
Quentin Avatar answered Nov 15 '22 04:11

Quentin