gcc vs clang - 使用“make_overload”可变参数 lambda 继承时的模糊重载

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

又一轮 clang vs gcc 的时间到了。 Live example on 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);
}

测试 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);
}

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

我可以给你一个解决方法。

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

在这里,我们继承了一堆不同的 parent 类型,每个类型都有一个 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 都应该喜欢它。

线性继承是sub-optimal:平衡二叉树更好,但需要更多工作。 (基本上,你拿一个包 Xs... 然后使用小心的 TMP 将它分成 Xs_front...Xs_back...,将它们放在 types<...> 包中,将它们转录到你的两个 parents,并执行 using blah::operator() 操作)。这是因为编译器对递归模板实例化和继承深度的限制往往比总模板实例化和继承的限制更浅 "volume".


中我们不必做这种线性继承:

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

因为他们增加了一个新地点,你可以...扩展。