4

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

#include <memory>
#include <type_traits>

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

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

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

4

1 回答 1

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(实例化点,编译器放置专门的模板代码)。

在实践中,大多数编译器将大多数函数模板的实际实例化延迟到翻译单元的末尾。一些实例化不能延迟,包括需要实例化以确定推导的返回类型的情况以及函数是 constexpr 并且必须评估以产生恒定结果的情况。一些编译器在第一次用于可能立即内联调用时会实例化内联函数。这有效地将相应模板特化的 POI 删除到翻译单元的末尾,这是 C++ 标准允许作为替代 POI 的(来自C++ Templates, The Complete Guide, 2nd Ed. , 14.3.2. Points of Instantiation,第 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已经遇到了时间。

不过,我不确定如何实际验证这一点,因此欢迎来自更精通编译器的人的任何输入。

于 2018-05-29T09:57:43.330 回答