SFINAE 关于具有默认参数的函数 - 自由函数与 operator()

SFINAE on functions with default parameters - free function vs operator()

我正在研究 以研究它如何处理带有默认参数的函数。令我惊讶的是,自由函数和 operator():

的结果不同
template <typename F>
auto func(F f) -> decltype(f(42))
{
    int a = 51;
    return f(51);
}

template <typename F>
auto func(F f) -> decltype(f(42, 42))
{
    int a = 0;
    int b = 10;
    return f(a, b);
}

int defaultFree(int a, int b = 0)
{
    return a;
}

auto defaultLambda = [](int a, int b = 0)
{
    return a;
};

int foo()
{
    return func(defaultFree);  
    //return func(defaultLambda);
}

Godbolt link

上面的 func(defaultFree) 版本编译时两个 func 模板都可用。正如预期的那样,它选择了第二个,因为默认参数不被视为函数签名的一部分。实际上,删除第二个 func 模板会导致编译错误。

然而,func(defaultLambda) 确实 not compile 由于歧义:两个 func 模板匹配。删除其中任何一个都会编译此版本。

(当然,如果你用类似的 operator() 手动编写 struct,也会发生同样的情况。最新的 gccclangMSVC大家也都同意。)

operator() 在未计算的 SFINAE 上下文中考虑默认参数而不是自由函数的原因是什么?

当您将自由函数作为参数传递时,它会进行函数到指针的转换。发生这种情况时,默认参数 (which is not a part of the function's type) 就会消失。它现在是一个指向带有两个参数的函数的指针,因此只有一个 SFINAE 检查可以通过它。

lambda 的类型没有经过这样的调整。未计算的表达式必须涉及 operator() 的重载解析,并且找到具有默认参数的声明,这允许它在具有单个参数的调用中使用。

当无捕获 lambda 被迫转换为函数指针时(例如 func(+defaultLambda);,由@YSC 提供),出于同样的原因,歧义消失了。

函数名称不是 C++ 对象的名称。

相反,当您使用函数名称时,会发生一堆转换。重载解析是根据调用或(隐式或显式)转换上下文完成的,并生成一个指针。

函数的默认参数是重载决策的一部分。它们永远不会作为函数指针类型的一部分传递。

您可以创建一个将函数名称转换为函数对象的简单包装器:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__( decltype(args)(args)... ) )

有了这个,你可以修改你的代码:

return func(OVERLOADS_OF(defaultFree));

并获取 func 和 SFINAE 考虑的默认参数,以产生歧义。

现在,OVERLOADS_OF(defaultFree) 是一个函数对象,SFINAE 测试它的参数是否可以传递给名为 defaultFree 的可调用对象。这允许传递 1 个参数。

函数对象不是函数。 lambda 是一个函数对象,OVERLOADS_OF 的 return 类型也是如此。可以传入函数对象并考虑其重载operator();他们可以记住他们的默认参数,做 SFINAE 等等

所以当你传递lambda时,两种可能性都是合法的。当你传递一个函数时,它变成了一个指向函数的无默认参数调用的函数指针,并且明确地不接受 1 个参数。


要解决您的问题,您应该使一个重载看起来比另一个更好。

一种方法是使用 ...:

namespace impl {
  template <typename F>
  auto func(F f,...) -> decltype(f(42))
  {
    int a = 51;
    return f(51);
  }

  template <typename F>
  auto func(F f, int) -> decltype(f(42, 42))
  {
    int a = 0;
    int b = 10;
    return f(a, b);
  }
}
template <typename F>
auto func(F f) -> decltype( impl::func(f, 0) )
{
  return impl::func(f, 0);
}

诀窍是当您通过 0.

int 优于 ...

你也可以更明确,生成像 "can be called with 1 argument"、"can be called with 2 arguments" 这样的特征,然后声明只有当你可以用 1 个而不是 2 个参数调用时才启用 1 个参数的情况。

还有基于标签分派的重载解析排序技术。