В соответствии с простой, навязчиво подсчитанной системой объектов, у меня есть template<typename T> class Handle
, который предназначен для создания экземпляра с подклассом CountedBase
, Handle<T>
содержит указатель на T
и его деструктор вызывает DecRef
(определено в CountedBase
) на этот указатель.
Обычно это может вызвать проблемы при попытке ограничить зависимости заголовка с помощью предварительных объявлений:
#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.
В качестве решения я переписал Handle<T>::~Handle()
следующее:
template<typename T>
Handle<T>::~Handle() {
reinterpret_cast<CountedBase*>(m_ptr)->DecRef();
}
Обратите внимание, что я использую reinterpret_cast
здесь вместо static_cast
, поскольку reinterpret_cast
не требует определения T
быть завершенным. Конечно, он также не будет выполнять настройку указателя для меня … но пока я осторожен с макетами (T
должен иметь CountedBase
как его самый левый предок, не должен наследовать от него виртуально, а на паре необычных платформ необходима некоторая дополнительная магия vtable), это безопасно.
Что будет действительно было бы хорошо, если бы я мог получить этот дополнительный слой static_cast
безопасность, где это возможно. На практике определение T
является обычно завершить в точке, где Handle::~Handle
создается, что делает это идеальным моментом, чтобы перепроверить, что T
на самом деле наследует от CountedBase
, Если он неполный, я мало что могу сделать … но если он полон, проверка работоспособности была бы хорошей.
Что подводит нас, наконец, к моему вопросу:
Есть ли способ сделать проверку во время компиляции, что T
наследуется от CountedBase
которая не приведет к (ложной) ошибке, когда T
не завершено?
Использование SFINAE на sizeof
чтобы проверить, завершен ли тип:
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;
}
Вывод (обратите внимание, что порядок уничтожения обратный):
static
reinterpret
Попытка с полным типом, который не происходит от CountedBase
дает:
main.cpp:16:5: error: static_cast from 'Oops *' to 'CountedBase *' is not allowed
При этом, я думаю, что более элегантный (и более явный) подход заключается во введении шаблона класса. incomplete<T>
такой, что Handle<incomplete<Foo>>
компилируется в reinterpret_cast
и все остальное пытается static_cast
,