在显式实例化期间,不完整的类型何时可以?

When are incomplete types okay during explicit instantiation?

本文关键字:类型 何时可 实例化      更新时间:2023-10-16

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

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

此外,我希望能够对Foo<T>用户隐藏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的检查?有没有一个更干净的解决方案,不涉及添加函数,但仍允许在Bar的定义之前放置template class Foo<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(实例化点,编译器放置专用模板代码(。

实际上,大多数通信器将大多数函数模板的实际实例化延迟到翻译单元的末尾。某些实例化不能延迟,包括需要实例化来确定推导的返回类型的情况,以及函数是constexpr并且必须计算以产生常量结果的情况。某些编译器在首次使用内联函数时会实例化内联函数,以可能立即内联调用。这有效地将相应模板专业化的 POI 删除到翻译单元的末尾,这是 C++ 标准允许作为替代 POI(来自C++ 模板,完整指南,第 2 版,14.3.2。实例化点》,第254页(

std::is_base_of 需要一个完整的类型,所以当它没有被RequiredIsBaseOf隐藏时,它可以作为函数模板部分实例化,is_base_of可能会导致插入 POI 的编译器尽快发出错误。

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

不过,我不确定如何实际验证这一点,因此欢迎更精通编译器的人提供任何意见。