【问题标题】:Conversion functions, std::is_base_of and spurious incomplete types: substitution failure IS an error转换函数,std::is_base_of 和虚假的不完整类型:替换失败是一个错误
【发布时间】:2021-08-29 01:27:35
【问题描述】:

我正在尝试实现转换函数运算符,并使用 std::is_base_of 来限制适用范围,但我遇到了问题。

#include <type_traits>

class Spurious;

class MyClassBase {};

template< typename T >
class MyClass: public MyClassBase {
public:
    template< typename U, std::enable_if_t< std::is_base_of<MyClassBase, U>::value, bool> = true >
    operator U () const {
        return U{}; // Complex initialization omitted for brevity
    }
};

class MyBool: public MyClass< bool > {};
class MyInt: public MyClass< int > {};

int do_stuff( int i, Spurious const & obj);
int do_stuff( int i, MyBool const & obj) {
    return i;
}

int main() {
    MyInt const obj;
    return do_stuff( 3, obj );
}

存在 Spurious 和相关函数定义意味着我在 is_base_of 实现中遇到编译器错误(compiler explorer;GCC:error: invalid use of incomplete type 'const class Spurious'Clang:error: incomplete type 'Spurious' used in type trait expression)。

我明白为什么你不能 is_base_of 一个不完整的类型,但我不太明白为什么“替换失败不是错误”在这里不适用。我原以为在模板扩展期间尝试 is_base_of 不完整类型会导致编译器中止转换尝试并继续下一个函数定义。这就是这里 is_base_of 语句的全部要点:忽略与模式不匹配的类。

不过,我的主要问题不是“为什么”,而是“下一步是什么”。有什么解决方法吗?是否有某种方法可以忽略诸如 Spurious 之类的东西,而不需要在此编译单元中完全定义它们?理想情况下,我只需修改转换运算符的定义(例如 SFINAE 模板参数)即可使其工作。

【问题讨论】:

  • SFINAE 不适用于不完整的类型,因为您不希望像is_base_of 这样的静态评估表达式 根据外部可能发生的变化返回不同的值。例如,两个不同的来源可能包含此代码作为标头——包含的顺序可能提供Spurious 的定义。然后突然你的评估顺序可能会改变。如果Spurious 继承自MyClassBase 怎么办?有定义的来源突然会出现歧义错误,但不完整的来源不会。

标签: c++ c++17 sfinae typetraits conversion-operator


【解决方案1】:

这不是 SFINAE,它肯定适用于不完整的类型:

template<class T,bool=false>
struct size : std::integral_constant<std::size_t,0> {};
template<class T>
struct size<T,!sizeof(T)> : std::integral_constant<std::size_t,sizeof(T)> {};
static_assert(size<char>::value==1);  // OK

struct A;
static_assert(size<A>::value==0);  // OK
struct A {};
static_assert(size<A>::value==0);  // OK!

这里的问题是std::is_base_of 具有“未定义的行为”(阅读:使用它会使程序格式错误,不需要诊断)如果这两种类型都是非联合类类型并且后者是不完整的。 SFINAE 没有涵盖这两个方面,因为它不在立即上下文中,而且因为它是 NDR。

这些编译器选择拒绝代码,这样做的好处是可以防止意外,例如上面的// OK!

您可以使用直接 SFINAE 方法避免此问题:

template< typename T >
class MyClass: public MyClassBase {
public:
    template< typename U, std::enable_if_t<
        (sizeof(U), std::is_base_of<MyClassBase, U>::value), bool> = true >
    operator U () const {
        return U{}; // Complex initialization omitted for brevity
    }
};

如果U 不完整,则模板参数的类型存在直接替换错误,这阻止实例化std::is_base_of 特化,即IFNDR。请注意,以这种方式在 SFINAE 中使用不完整类型仍然是相当危险,因为在相关类型完整的上下文中实例化的相同特化可能会产生不同的行为(也是 IFNDR,每 [temp.point]/7)。

【讨论】:

  • 您提供的代码无法编译,因为第二个 size 定义应该是部分特化。但即使将其定义为 size&lt;T,!sizeof(T)&gt; 也会因 gcc 失败,因为 “模板参数 '!sizeof (T)' 涉及模板参数” (source)。我不相信您有任何方法可以检测 T 类型是否不完整——因为语言不需要返回 sizeof 不完整类型的值。如果您可以检测到它,那么这将通过在编译时使用两个不同的值定义 size&lt;A&gt; 来违反 ODR
  • @Human-Compiler:最新版本的 GCC 和 Clang 接受它(经过简单修复后)。我们可以使用decltype 使其明确可接受; ODR 问题是非常真实的,但它们并没有使其不正确。
  • 您是否碰巧有任何关于decltype(sizeof(&lt;incomplete type&gt;)) 格式良好的参考资料?我的理解是sizeof(&lt;incomplete type&gt;) 格式错误,因此即使decltype 格式错误(不兼容SFINAE)。 [expr.sizeof]/1 的 C++ 标准声明它不能应用于不完整的类型——而不是它必须有条件地不返回值。是否有一个单独的段落让我不知道这个 SFINAE 友好?
  • @Human-Compiler:它是 SFINAEable 因为它的格式不正确([temp.deduct.general]/8,尽管 /11 中的原因列表仅适用于无效类型)。
  • 我仍然不完全理解为什么 SFINAE 不适用。我可以理解明确的未定义行为“胜过” SFINAE,但我没有得到您的即时上下文/NDR 评论。 ——无论如何,“下一步是什么”的主要问题仍未解决。特别是如果 is_base_of 可以调用鼻恶魔,如果您不确定哪些类型实际上会被输入它,这似乎使得在模板化上下文中使用它很危险。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-03-11
  • 1970-01-01
  • 1970-01-01
  • 2018-06-26
  • 2016-02-27
  • 2022-01-08
  • 1970-01-01
相关资源
最近更新 更多