重载解析和通用参考参数

Overload resolution and universal reference parameters

本文关键字:参考 参数 重载      更新时间:2023-10-16

下面的代码可以正常工作,并且发现了预期的重载:

struct HasBuzz
{
    void buzz() const {}
};
struct NoBuzz {};
template <typename T>
void foo(T const& t)
{
    t.buzz();
}
void foo(NoBuzz const&){}
int main()
{
    foo(HasBuzz{});
    foo(NoBuzz{});
}
然而,如果我用"通用引用"版本替换第一个重载,那么它就不再工作了。NoBuzz没有找到正确的过载
struct HasBuzz
{
    void buzz() const {}
};
struct NoBuzz {};
template <typename T>
void foo(T&& t)
{
    t.buzz();
}
void foo(NoBuzz const&){}
int main()
{
    foo(HasBuzz{});
    foo(NoBuzz{}); // error: NoBuzz has no member function buzz
}

我怎么做才能让它工作?

简单的解决方案

添加一个重载,可通过NoBuzz类型的右值调用。

void foo(NoBuzz const&){ }; 
void foo(NoBuzz&&)     { }; // overload for rvalues


注意:根据您的实际用例,这可能还不够,因为如果您传递一个非const 左值类型NoBuzzfoo,您仍然会实例化模板,因为两个NoBuzz重载不匹配。

在这篇文章的最后是一个更复杂,但肯定更干净的解决方案。


 template<class T>
 void foo (T&&);            // (A)
 void foo (NoBuzz const&);  // (B)
你的代码片段的问题是你的模板(A)可以用这样一种方式实例化,它比你的重载(B)更匹配。


当编译器看到您正在尝试调用名为foo的函数,其参数是NoBuzz类型的右值时,它将查找所有名为foo的函数,其参数为NoBuzz

假设它从模板(A)开始,这里可以看到T&&可以演绎为任何引用类型(lvaluervalue),因为我们传递的是rvalue T = NoBuzz

对于T = NoBuzz,实例化的模板在语义上相当于:

void foo (NoBuzz&&); // (C), instantiated overload of template (A)


它将继续你的过载(B)。该重载接受一个const左值引用,它可以绑定到左值和右值;但是我们之前的模板实例(C)只能绑定到右值

因为(C)是比(B)更好的匹配,将右值绑定到T&&而不是U const&,选择了重载,您得到了您在帖子中描述的行为。


先进解决方案

我们可以使用一种叫做SFINAE的技术,如果传递的类型不实现.buzz (),则有条件地使模板无法调用。

template <typename T>
auto foo(T&& t) -> decltype (t.buzz ())
{
  return t.buzz();
}

上面的解决方案使用了很多 c++ 11的新特性,详细信息在这里:

  • wikipedia.org - c++ - auto
  • wikipedia.org - decltype
  • 可能有尾随返回类型的条件重载吗?

refp的回答解释了您所看到的行为,并提供了一个可能的解决方案。另一个选择是确保foo函数模板不进入重载解析的候选集合,除非T有一个名为buzz()的成员函数。

template <typename T>
auto foo(T&& t)
    -> decltype((void)(t.buzz()), void())
{
    t.buzz();
}

在做了这个改变之后,当你传递一个NoBuzz的实例时,foo(NoBuzz const&)过载将被选中。现场演示

关于末尾返回类型的decltype表达式中发生的事情的详细解释可以在这里找到。我在这里做的唯一不同的事情是使用三个子表达式,中间一个是void(),以防止用户定义的operator,被选中,我将第一个表达式的结果强制转换为void;在这两种情况下,目的和结果是相同的。