我正在尝试创建一种自动创建包装对象的包装类:
#include <memory>
#include <type_traits>
template<typename T>
class Foo {
std::unique_ptr<T> _x;
public:
Foo(); // will initialize _x
};
此外,我希望能够隐藏T
用户Foo<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的检查?是否有更清晰的解决方案,不涉及添加功能,但仍允许在template class Foo<Bar>;
的定义之前放置Bar
?
答案 0 :(得分:1)
版本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(实例化点,编译器根据规则放置专用模板代码)。
实际上,大多数commpilers都会将大多数函数模板的实际实例化延迟到翻译单元的末尾。某些实例化不能被延迟,包括需要实例化以确定推断的返回类型的情况以及函数是constexpr并且必须被评估以产生恒定结果的情况。一些编译器在首次使用内联函数时可以实例化内联函数。这有效地将相应模板特化的POI移除到翻译单元的末尾,这是C ++标准允许的替代POI(来自 C ++模板,完整指南,第2版。,14.3 .2。实例化点,第254页
std::is_base_of需要一个完整的类型,因此如果RequiredIsBaseOf
没有隐藏它,可以将部分实例化的函数模板隐藏起来,is_base_of
可以引导将POI插入的编译器尽快发出错误。
正如t.niese所述,任何一个版本都可以使用-std=c++17
标志的godbolt上的任何gcc版本。我的猜测是gcc做了最晚的POI之一,而clang使用了第一个合法的,#2
。使用具有从属名称的函数模板(当首次遇到RequiredIsBaseOf时,仍然必须填入T)会强制clang对依赖类型执行第二次查找运行,此时遇到Bar
。< / p>
我不确定如何实际验证这一点,因此欢迎来自更精通编译器的人的任何意见。