为啥我的 SFINAE 表达式不再适用于 GCC 8.2?

Posted

技术标签:

【中文标题】为啥我的 SFINAE 表达式不再适用于 GCC 8.2?【英文标题】:Why do my SFINAE expressions no longer work with GCC 8.2?为什么我的 SFINAE 表达式不再适用于 GCC 8.2? 【发布时间】:2018-08-10 13:31:27 【问题描述】:

我最近将 GCC 升级到 8.2,我的大部分 SFINAE 表达式都停止工作了。

以下内容有所简化,但演示了问题:

#include <iostream>
#include <type_traits>

class Class 
public:
    template <
        typename U,
        typename std::enable_if<
            std::is_const<typename std::remove_reference<U>::type>::value, int
        >::type...
    >
    void test() 
        std::cout << "Constant" << std::endl;
    

    template <
        typename U,
        typename std::enable_if<
            !std::is_const<typename std::remove_reference<U>::type>::value, int
        >::type...
    >
    void test() 
        std::cout << "Mutable" << std::endl;
    
;

int main() 
    Class c;
    c.test<int &>();
    c.test<int const &>();
    return 0;

C++ (gcc) – Try It Online

C++ (clang) – Try It Online

旧版本的 GCC(不幸的是我不记得我之前安装的确切版本)以及 Clang 编译上面的代码就好了,但是 GCC 8.2 给出了一个错误说明:

:在函数'int main()'中:
:29:19: 错误: 重载 'test()' 的调用不明确
     c.test();
                   ^
:12:10: 注意:候选人:'void Class::test() [with U = int&;类型名 std::enable_if::type>::value>::type ... = ]'
     无效测试()
          ^~~~
:22:10: 注意:候选人:'void Class::test() [with U = int&;类型名 std::enable_if::type>::value)>::type ... = ]'
     无效测试()
          ^~~~
:30:25: 错误: 重载 'test()' 的调用不明确
     c.test();
                         ^
:12:10: 注意:候选人:'void Class::test() [with U = const int&;类型名 std::enable_if::type>::value>::type ... = ]'
     无效测试()
          ^~~~
:22:10: 注意:候选人:'void Class::test() [with U = const int&;类型名 std::enable_if::type>::value)>::type ... = ]'
     无效测试() 

通常情况下,不同的编译器和编译器版本以不同的方式处理相同的代码,我假设我正在调用未定义的行为。标准对上述代码有什么说法?我做错了什么?


注意:问题不在于解决此问题的方法,我想到了几种方法。问题是为什么这不适用于 GCC 8 - 它是标准未定义的,还是编译器错误?

注意 2: 由于每个人都在使用默认的 void 类型的 std::enable_if,因此我将问题更改为使用 int。问题依然存在。

注3:GCC bug report created

【问题讨论】:

从 godbolt 开始,它一直工作到 gcc 7.3 (你可以在程序集中看到它做了正确的事情)。 ::type 之后用... 扩展什么? 删除省略号,将默认的void 替换为int 并添加默认值可以解决问题,是的。真正的问题是,上述代码以前运行良好,但在 GCC 8 中不再运行的原因是什么? @xskxzr 根据这个问题,void... 仅适用于空参数包,这正是这里所需要的。即使它是非法的,在std::enable_if 中用int 替换默认的void 也不会改变问题的任何内容。 最小示例:godbolt.org/g/P9z1pt gcc7.1 OK gcc 8.x KO 【参考方案1】:

这是我的看法。总之,clang是对的,gcc有回归。

我们有照[temp.deduct]p7:

替换发生在函数类型和模板参数声明中使用的所有类型和表达式中。 [...]

这意味着无论包是否为空,都必须进行替换。因为我们仍处于直接上下文中,所以这是 SFINAE-able。

接下来我们知道可变参数确实被认为是一个实际的模板参数;来自[temp.variadic]p1

模板参数包是接受零个或多个模板参数的模板参数。

而[temp.param]p2 表示允许哪些非类型模板参数:

非类型模板参数应具有以下类型之一(可选 cv 限定):

一种文字类型,具有强结构相等性([class.compare.default]),没有可变或易失的子对象,并且如果存在默认成员运算符,则将其声明为公共的,

左值引用类型,

包含占位符类型 ([dcl.spec.auto]) 的类型,或

推导类类型的占位符 ([dcl.type.class.deduct])。

请注意,void 不符合要求,您的代码(如发布的)格式不正确。

【讨论】:

问题不在于std::enable_ifvoid默认类型!如果我正确理解您的答案,SFINAE 应该适用于不同的类型(例如int)。它不在 GCC 8.2 下。 @zennehoy 这就是为什么我在第二句话中说“如果不是void,则回归”:) 你介意减少你的答案吗?我现在在问题中将int 指定为std::enable_if 的类型,因为我真的不打算开始讨论void...,这原本是我简化代码的遗物。请注意,其他人得出的结论与您不同:***.com/a/23711944/694509 :) @zennehoy 我不太明白答案;不过谢谢 @zenn 其他答案不正确,因为这不是包扩展。它只是一个非扩展模板参数包,如果你给它一个名字,它可以在其他地方扩展。【参考方案2】:

我不是语言律师,但下面的引用不能与问题有某种联系吗?

[temp.deduct.type/9]: 如果 Pi 是包扩展,则将 Pi 的模式与 A 的模板参数列表中的每个剩余参数进行比较。每次比较都会推导出由 Pi 扩展的模板参数包中后续位置的模板参数。

在我看来,由于模板参数列表中没有剩余参数,因此没有比较模式(其中包含enable_if)。如果没有比较,那么也没有扣除,我相信扣除后会发生替代。因此,如果没有替换,则不会应用 SFINAE。

如果我错了,请纠正我。我不确定这个特定的段落是否适用于此,但在 [temp.deduct] 中有更多类似的关于包扩展的规则。此外,此讨论可以帮助更有经验的人解决整个问题:https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/JwZiV2rrX1A。

【讨论】:

有趣的是,讨论得出的结论是,这是 Clang 中的一个错误,随后被修复!标准中似乎有相当多的不确定性......【参考方案3】:

部分答案:将typename = typename enable_if&lt;...&gt;, T=0 与不同的Ts 一起使用:

#include <iostream>
#include <type_traits>

class Class 
public:
    template <
        typename U,
        typename = typename std::enable_if_t<
            std::is_const<typename std::remove_reference<U>::type>::value
        >, int = 0
    >
    void test() 
        std::cout << "Constant" << std::endl;
    

    template <
        typename U,
        typename = typename  std::enable_if_t<
            !std::is_const<typename std::remove_reference<U>::type>::value
        >, char = 0
    >
    void test() 
        std::cout << "Mutable" << std::endl;
    
;

int main() 
    Class c;
    c.test<int &>();
    c.test<int const &>();
    return 0;

(demo)

仍在试图弄清楚 std::enable_if&lt;...&gt;::type... 到底是什么意思知道 default type is void

【讨论】:

见***.com/a/23711944/694509,基本要求参数包为空。随意添加 int 来替换默认的 void 类型 - 这不会改变问题的任何内容。 @zennehoy 注意:如果没有添加 tparam (int & char),您的两个模板函数具有相同的模板签名,因此会产生歧义。我不明白为什么它以前会起作用。 怎么样?其中一个是template &lt;typename U, void...&gt;,另一个是SFINAE,即template &lt;typename U, [error: std::enable_if has no member type]...&gt; @YSC: typename = std::enable_if_t&lt;cond&gt; 需要“奇怪”的额外虚拟参数,最好使用std::enable_if_t&lt;cond, int&gt; = 0

以上是关于为啥我的 SFINAE 表达式不再适用于 GCC 8.2?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Office 365 连接到 office 按钮不再适用于新的 Microsoft Teams 组?

如何在 iOS 中添加双拇指滑块?以及为啥这个双滑块库不再适用于 xcode 7

为啥说分布式事务不再适用于微服务架构

为啥 GCC 说“不再支持命名返回值”?

首先引导手机?为啥悬停仍然适用于移动设备?如何禁用悬停?

为啥 void_t 在 SFINAE 中不起作用但 enable_if 起作用