GCC vs clang -使用' make_overload '可变lambda继承时的模糊重载

gcc vs clang - ambiguous overload when using `make_overload` variadic lambda inheritance

本文关键字:继承 lambda 重载 模糊 可变 make clang vs 使用 GCC overload      更新时间:2023-10-16

又是一轮clang vs gcc的时间了。godbolt.org上的实例。


测试0:重载可调用对象

struct Trad
{
    auto operator()(int)    { return 1; }
    auto operator()(float)  { return 2; }
    auto operator()(double) { return 3; }
};
int main()
{
    assert(Trad{}(1) == 1);
    assert(Trad{}(1.f) == 2);
    assert(Trad{}(1.0) == 3);
}
  • g++ 5.2编译并运行。
  • clang++ 3.5 (及以后的版本)编译并运行。

Test 1:重载可调用对象,通过lambda继承生成

template <typename... TFs>
struct overload_set : TFs...
{
    overload_set(TFs... fs) : TFs(fs)... {}
};
template <typename... TFs>
auto overload(TFs&&... fs)
{
    return overload_set<TFs...>{fs...};
}
int main()
{
    auto func = overload 
    (
        [](int)    { return 1; }, 
        [](float)  { return 2; }, 
        [](double) { return 3; }
    );
    assert(func(1) == 1);
    assert(func(1.f) == 2);
    assert(func(1.0) == 3);
}
  • g++ 5.2无法编译。

    • 错误:请求成员'operator()'是不明确的

  • clang++ 3.5 (及以后的版本)编译并运行。


这里哪个编译器是正确的?

我可以给你一个变通办法。

template <typename... TFs>
struct overload_set : TFs...
{
  overload_set(TFs... fs) : TFs(fs)... {}
};

在这里,我们继承了一堆不同的父类型,每个父类型都有一个operator()。它们不会(至少在gcc中)以您想要的方式重载。

为了解决这个问题,我们线性继承并通过using()向下携带:

template<class...Ts>
struct inherit_linearly;
template<>
struct inherit_linearly<>{};
template<class T0, class...Ts>
struct inherit_linearly<T0, Ts...>:
  T0, inherit_linearly<Ts...>
{
   using T0::operator();
   using inherit_linearly<Ts...>::operator();
   template<class A0, class...As>
   inherit_linearly( A0&&a0, As&&...as ):
     T0(std::forward<A0>(a0)),
     inherit_linearly<Ts>(std::forward<As>(as)...) 
   {}
};

现在我们替换overload_set如下:

template <typename... TFs>
struct overload_set : inherit_linearly<TFs...>
{
  using inherit_linearly<TFs...>::operator();
  overload_set(TFs... fs) :
    inherit_linearly<TFs...>(std::forward<TFs>(fs)...)
  {}
};

和GCC和clang应该都喜欢它。

线性继承是次优的:平衡二叉树更好,但需要更多的工作。(基本上,你拿一个Xs...包,然后用TMP把它分成Xs_front...Xs_back...,把它们放在types<...>包里,把它们转录给你的父母,然后做using blah::operator()的事情)。这是因为编译器对递归模板实例化和继承深度的限制往往比总模板实例化和继承"量"的限制更浅。


在c++17中,我们不需要做这种线性继承:

template <typename... TFs>
struct overload_set : TFs...
{
  using TFs::operator()...;
  overload_set(TFs... fs) : TFs(fs)... {}
};

因为他们增加了一个新的位置你可以做...扩展