Опционально проверено литье на возможно неполный тип

В соответствии с простой, навязчиво подсчитанной системой объектов, у меня есть 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 не завершено?

[Обычный отказ от ответственности: я знаю, что есть потенциально небезопасные и / или UB аспекты использования неполных типов таким способом. Тем не менее, после большого количества кросс-платформенного тестирования и профилирования, я решил, что это наиболее практичный подход, учитывая определенные уникальные аспекты моего варианта использования. Меня интересует вопрос проверки во время компиляции, а не общий обзор кода.]

5

Решение

Использование 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,

2

Другие решения


По вопросам рекламы [email protected]