为什么它是一个使用GCC的令人震惊的函数调用?模板扣减失败

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

本文关键字:函数调用 失败 为什么 一个 GCC      更新时间:2023-10-16

在我看来,我无法用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。然而,问题与我的、有意义的、真实的代码中的问题相同。

结果:

  • 没有解决方法的叮当声会导致内部编译器错误
  • 没有任何解决方法的GCC导致对的函数调用不明确
error(const Base<T, Ns...>&)

事实上,我预计不需要变通。clang应该能够推导出模板参数。我看不出GCC是如何得出函数调用不明确的想法的,在我看来这是非常清楚的。我是做错了什么,还是假设模板扣减是不符合C++(17)标准的?

其他意外行为:启用其中一个GCC解决方案后,将调用包装器函数两次。我不确定这是意料之中的事。或者换句话说:我不确定是先进行模板推导还是转换为基数。Clang在这里的观点似乎与GCC不同。也就是说,先转换到基准的引用,但之后模板推导失败(见错误报告)。哪一个是对的?

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

  • 让我们将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 的(复杂)规则

    我们将这两种方法都作为可行的函数,两者都是模板,所以我们使用更专业的模板

    我(作为clang)理解#1比#2更专业,但不适用于gcc

    演示

    我会说gcc bug。

  • 让我们简化clang to的问题:

    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,所以叮当的bug。

    对于分辨率,#2是一个精确匹配,而#1需要将派生类转换为其基类。

    gcc同意调用#2。

    演示

我不知道这对您来说是否可以接受,但我为您的原始问题提出了一个不同的解决方案:在模板-模板参数和容器中进行转换。

我的意思是。。。而不是

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
}