c – 在显式实例化期间什么时候不完整类型没问题?

我正在尝试创建一种自动创建包装对象的包装类:

#include <memory>
#include <type_traits>

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

此外,我希望能够隐藏来自Foo< T>的用户的T的实现细节. (对于PIMPL pattern).对于单翻译单元示例,假设我有

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的检查?是否有一个更清洁的解决方案,不涉及添加功能,但仍然允许放置模板类Foo< Bar&gt ;;在定义Bar之前?

最佳答案 版本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(实例化点,编译器根据规则放置专用模板代码).

In practice, most commpilers delay the actual instantiation of most function templates to the end of the translation unit. Some instantiations cannot be delayed, including cases where instantiation is needed to determine a deduced return type and cases where the function is constexpr and must be evaluated to produce constant result. Some compilers instantiate inline functions when they’re first used to potentially inline the call right away. This effectively removes the POIs of the corresponding template specializations to the end of the translation unit, which is permitted by the C++ standard as an alternative POI (from C++ Templates, The Complete Guide, 2nd Ed., 14.3.2. Points of Instantiation, p.254)

std::is_base_of需要一个完整的类型,因此当它不被RequiredIsBaseOf隐藏时,可以将部分实例化的函数模板隐藏起来,is_base_of可以引导编译器尽快插入POI以发出错误.

正如t.niese所述,任何版本的godbolt上的gcc都可以采用-std = c 17标志.我的猜测是gcc做了最晚的POI之一,而clang使用了第一个合法的,#2.使用具有依赖名称的函数模板(当首次遇到RequiredIsBaseOf时,仍然必须填写T)强制clang对依赖类型执行第二次查找运行,此时遇到了Bar.

我不确定如何实际验证这一点,所以来自更精通编译器的人的任何意见都会受到欢迎.

点赞