Когда неполные типы в порядке во время явной реализации? - PullRequest
0 голосов
/ 29 мая 2018

Я пытаюсь создать своего рода класс-оболочку, который автоматически создает обернутый объект:

#include <memory>
#include <type_traits>

template<typename T>
class Foo {
    std::unique_ptr<T> _x;
public:
    Foo();  // will initialize _x
};

Кроме того, я хочу скрыть детали реализации T от пользователей Foo<T> (для шаблона PIMPL ).Для примера с единичным переводом, предположим, у меня есть

struct Bar;  // to be defined later

extern template class Foo<Bar>;
// or just imagine the code after main() is in a separate translation unit...

int main() {
    Foo<Bar> f;  // usable even though Bar is incomplete
    return 0;
}

// delayed definition of Bar and instantiation of Foo<Bar>:

template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) { }

template class Foo<Bar>;
struct Bar {
    // lengthy definition here...
};

Это все работает отлично.Однако, если я хочу, чтобы потребовал, чтобы T был производным от другого класса , компилятор жалуется, что Bar неполон:

struct Base {};

template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) {
    // error: incomplete type 'Bar' used in type trait expression
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}

Попытка выполнить ту же проверку с использованием static_cast завершается аналогично:

template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) {
    // error: static_cast from 'Bar *' to 'Base *', which are not related by inheritance, is not allowed
    // note: 'Bar' is incomplete
    (void)static_cast<Base*>((T*)nullptr);
}

Однако, если добавить еще один уровень шаблонизации функций, я могу заставить эту работу:

template<typename Base, typename T>
void RequireIsBaseOf() {
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}

// seems to work as expected
template<typename T>
Foo<T>::Foo() : _x((RequireIsBaseOf<Base, T>(), std::make_unique<T>())) { }

Обратите внимание, чтодаже следующее по-прежнему вызывает ошибку неполного типа, несмотря на похожую структуру:

// error: incomplete type 'Bar' used in type trait expression
template<typename T>
Foo<T>::Foo() : _x((std::is_base_of<Base, T>::value, std::make_unique<T>())) { }

Что здесь происходит?Не задерживает ли дополнительная функция проверку static_assert?Есть ли более чистое решение, которое не предполагает добавления функции, но все же позволяет поместить template class Foo<Bar>; перед определением Bar?

1 Ответ

0 голосов
/ 29 мая 2018

Версия 1

// #1
// POI for Foo<Bar>: class templates with no dependent types are instantiated at correct scope BEFORE call, with no further lookup 
// after first parse
int main() {
    Foo<Bar> f;  // usable even though Bar is incomplete
    return 0;
}

// delayed definition of Bar and instantiation of Foo<Bar>:


struct Base {};

// error: incomplete type 'Bar' used in type trait expression
template<typename T>
Foo<T>::Foo() : _x(std::make_unique<T>()) {
    // error: incomplete type 'Bar' used in type trait expression
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}
// #2
// POI for static_assert: function templates with no dependent types are
// instantiated at correct scope AFTER call, but no further lookup is
// performed, as with class templates without dependent types
// is_base_of forces the compiler to generate a complete type here

template class Foo<Bar>;
struct Bar : private Base {
    // lengthy definition here...
};

версия 2:

    struct Base {};
template<typename Base, typename T>
void RequireIsBaseOf() {
    static_assert(std::is_base_of<Base, T>::value, "T must inherit from Base");
}

// seems to work as expected
template<typename T>
Foo<T>::Foo() : _x((RequireIsBaseOf<Base, T>(), std::make_unique<T>())) { }
// #3
// is_base_of does not force any complete type, as so far, only the 
// incomplete type of RequiredIsBaseOf is around.

template class Foo<Bar>;
struct Bar : private Base {
    // lengthy definition here...
};
// #3
// POI for RequiredIsBaseOf: function templates WITH dependent types are instantiated at correct scope AFTER call, after the second phase of two-phase lookup is performed. 

Вот в чем, на мой взгляд, проблема: любая точка после # 2 является разрешенным POI ( Точка экземпляра), где компилятор помещает специализированный код шаблона) в соответствии с правилами.

На практике большинство компиляторов откладывают фактическую реализацию большинства шаблонов функций до конца блока перевода.Некоторые экземпляры не могут быть отложены, в том числе случаи, когда для определения типа возвращаемого результата требуется определение экземпляра, и случаи, когда функция является constexpr и должна быть оценена для получения постоянного результата.Некоторые компиляторы создают экземпляры встроенных функций, когда они впервые используются, чтобы потенциально встроить вызов сразу.Это эффективно удаляет POI соответствующих специализаций шаблонов до конца модуля перевода, что допускается стандартом C ++ в качестве альтернативного POI (из C ++ Templates, The Complete Guide, 2nd Ed. , 14.3.2. Точки инстанции , с.254)

std :: is_base_of требуется полный тип, поэтому, когда он не скрыт RequiredIsBaseOf,который может быть частично создан как шаблон функции, is_base_of может привести к тому, что компиляторы, которые вставят POI, как можно скорее, выдадут ошибку.

Как отмечает t.niese, любая версия gcc на godbolt, которая может принимать флаг -std=c++17, подходит для любой версии.Я предполагаю, что gcc выполняет одну из последних задач POI, в то время как clang использует первую легальную, #2.Использование шаблона функции с зависимыми именами (при первом обнаружении RequiredIsBaseOf, T все еще должен быть заполнен) вынуждает clang выполнить второй прогон поиска для зависимого типа, к которому время Bar было найдено.

Хотя я не уверен, как на самом деле это проверить, поэтому приветствуются любые комментарии людей, более сведущих в компиляторах.

...