为什么它是使用 GCC 的模糊函数调用?模板推导失败?

Why is it an ambigious function call using GCC? Template deduction failing?

在我看来,我无法使用 GCC 或 clang 编译当前有效的 C++(17) 代码。

我最近设法用 clang 编译我的(在我看来)有效的 C++17 代码(错误报告:https://bugs.llvm.org/show_bug.cgi?id=40305)。后来我更改了代码,尝试用 GCC 编译代码时也出现错误。

我设法隔离了有问题的代码部分,并为两个编译器找到了可能的解决方法,这些方法也适用于我的真实代码:

#include <iostream>
#include <utility>

template<class T, int... Ns>
class Tensor
{
};

template<class T, int... Ns>
class Base
{
};

template<class T, int... Ns>
class Derived : public Base<T, Ns...>
{
};

template<class T, int... Ns>
decltype(auto) convert(Base<T, Ns...> const &a)
{
    return a;
}

template<class T, int... Ns>
auto convert(Tensor<T, Ns...> const &)
{
    return Derived<T, Ns...>();
}

#ifdef WGCC1 // First work-around for GCC
template<class T, int... Ns>
void error(Base<T, Ns...> const &arg)
{
    std::cout << "Function" << std::endl;
}
#endif

template<class... Ts, int... Ns>
void error(Base<Ts, Ns...> const &... args)
{
    std::cout << "Function" << std::endl;
}

template<class... Ts
#ifdef WGCC2 // Second work-around for GCC
    >
#else
    ,
    class = std::enable_if_t<
        std::conjunction<std::is_class<std::remove_reference_t<Ts>>...>{}>>
#endif
void error(Ts &&... args)
{
    std::cout << "Wrapper: ";
    ((std::cout << typeid(args).name() << " "), ...);
    std::cout << std::endl;
#ifdef WCLANG // Work-around for clang, see:
              // https://bugs.llvm.org/show_bug.cgi?id=40305
    return error(convert(convert(std::forward<Ts>(args)))...);
#else
    return error(convert(std::forward<Ts>(args))...);
#endif
}

int main()
{
    Tensor<int, 4, 4> a;
    error(a);
}

参见:https://godbolt.org/z/L5XVgL

注意:显然此代码不再有意义,即 SFINAE 检查 std::is_class。但是,问题与我的有意义的真实代码中的问题相同。

结果:

error(const Base<T, Ns...>&)

实际上我希望不需要解决方法。 clang 应能够推导出模板参数。 我看不出 GCC 是如何得出函数调用不明确的想法的,在我看来这很清楚。 我是不是做错了什么,或者假设模板推导起作用但根据 C++(17) 标准不起作用?

其他意外行为: 在启用 GCC 解决方法之一的情况下,包装器函数被调用两次。我不确定这是预期的。或者换句话说:我不确定是否首先完成模板推导或转换为基础。 Clang 在这里似乎与 GCC 有不同的看法。 IE。转换为对 base 的引用首先完成,但是之后模板推导失败(请参阅错误报告)。哪一个是正确的?

更新:也为 GCC 提交了错误:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88907

我不知道你是否可以接受,但我为你原来的问题提出了一个不同的解决方法:在模板-模板参数中转换容器。

我的意思是...而不是

template <class... Ts, int... Ns>
void error(Base<Ts, Ns...> const &... arg)
 { }

我提议

template <template <typename, int...> class C, typename ... Ts, int ... Ns>
void error(C<Ts, Ns...> const &...)
 { }

如果你想确定C是从Base派生出来的,你可以通过SFINAE强加这个;使用 C++17 模板折叠,你可以写

template <template <typename, int...> class C, typename ... Ts, int ... Ns>
std::enable_if_t<(... && std::is_base_of_v<Base<Ts, Ns...>, C<Ts, Ns...>>)> 
      error(C<Ts, Ns...> const &...)
 { }

您还可以在模板模板的可变列表中转换 C 并接受 BaseDerived

的混合
template <template <typename, int...> class ... Cs, typename ... Ts, int ... Ns>
std::enable_if_t<(... && std::is_base_of_v<Base<Ts, Ns...>, Cs<Ts, Ns...>>)>
      error(Cs<Ts, Ns...> const &...)
 { }

下面是一个完整的编译(g++和clang++)例子

#include <type_traits>

template <typename, int...>
class Base {};

template <typename, int...>
class Wrong {};

template <typename T, int... Ns>
class Derived : public Base<T, Ns...> {};


template <template <typename, int...> class ... Cs, typename ... Ts, int ... Ns>
std::enable_if_t<(... && std::is_base_of_v<Base<Ts, Ns...>, Cs<Ts, Ns...>>)>
      error(Cs<Ts, Ns...> const &...)
 { }

int main ()
 {
   Base<int, 1, 2, 3> a;
   Base<long, 1, 2, 3> b;

   error(a, b);

   Derived<int, 1, 2, 3> c;
   Derived<long, 1, 2, 3> d;

   error(c, d);

   error(a, c, b, d);

   Wrong<int, 1, 2, 3> e;
   Wrong<long, 1, 2, 3> f;

   //error(e, f); // compilation error  
   //error(a, c, e, b, d, f); // compilation error
 } 
  • 让 gcc 的问题简化为:

    template<class... Ts, int... Ns>
    void error(Base<Ts, Ns...> const &... args) {} // #1
    
    template<class... Ts,
        std::enable_if_t<
            std::conjunction<std::is_class<std::remove_reference_t<Ts>>...>{}, int> = 0>
    void error(Ts &&... args) {} #2
    
    int main()
    {
        const Base<int, 4> b;
    
        error(b); // call #1
    }
    

    遵循 overload_resolution

    的(复杂)规则

    我们将这两种方法都作为可行的函数, 两者都是模板,所以我们使用 the more specialized template

    我(作为 clang)明白 #1 比 #2 更专业,但不是 gcc

    Demo

    我会说 gcc 错误。

  • 让 clang 的问题简化为:

    template<class... Ts, int... Ns>
    void error(Base<Ts, Ns...> const &... args) {} // #1
    
    template<class... Ts,
        std::enable_if_t<
            (... && std::is_class<std::remove_reference_t<Ts>>::value), int> = 0>
    void error(Ts&&... args) {} // #2
    
    int main()
    {
        Derived<int, 4, 4> d;
        error(d); // call #2
    }
    

    ICE 总是一个 bug,所以 clang 的 bug。

    为了解决问题,#2 是完全匹配,而#1 需要将派生 class 转换为其基数。

    gcc 同意调用#2。

    Demo