如何检测某些可调用对象是否采用右值引用?

How to detect whether some callable takes a rvalue reference?

我一直在尝试编写一个 trait 来确定某些可调用对象是否将右值引用作为其第一个参数。这允许一些元编程调整在调用由外部代码提供的可调用对象时是否使用移动或复制语义(实际上是重载用户提供的可调用类型)。

#include <functional>
#include <iostream>
#include <type_traits>

// Does the callable when called with Arg move?
template<class F, class Arg> struct is_callable_moving
{
  typedef typename std::decay<Arg>::type arg_type;
  typedef typename std::function<F(arg_type)>::argument_type parameter_type;
  static constexpr bool value = std::is_rvalue_reference<parameter_type>::value;
};

int main(void)
{
  auto normal = [](auto) {};    // Takes an unconstrained input.
  auto moving = [](auto&&) {};  // Takes a constrained to rvalue ref input.
  std::cout << "normal=" << is_callable_moving<decltype(normal), int>::value << std::endl;
  std::cout << "moving=" << is_callable_moving<decltype(moving), int>::value << std::endl;  // should be 1, but isn't
  getchar();
  return 0;
}

以上显然行不通,但它有望解释我正在寻找的内容:我想检测将其参数限制为仅作为右值引用的可调用对象。

请注意,Get lambda parameter type 等其他 Stack Overflow 答案在这里没有用,因为我需要支持 C++ 14 通用 lambda(即采用自动参数的那些),因此需要基于获取地址的技巧lambda 类型内的调用运算符将​​因无法解析重载而失败。

您会注意到 is_callable_working 采用 Arg 类型,可调用 F 的正确重载将通过 F(Arg) 找到。我想检测的是 F(Arg) 的可用重载是 F::operator()(Arg &&) 还是 F::operator()()。我想如果 F() 的模糊重载可用,例如F(Arg)F(Arg &&) 那么编译器都会出错,但是 [](auto) 不应该与 [](auto &&).

混淆

编辑: 希望澄清我的问题。我真的想问 C++ 元编程是否可以检测参数的约束。

编辑 2: 这里有更多说明。我的确切用例是这样的:

template<class T> class monad
{
  ...
  template<class U> monad<...> bind(U &&v);
};

其中 monad<T>.bind([](T}{}) 通过副本获取 T,我希望 monad<T>.bind([](T &&){}) 通过右值引用获取 T(即可调用对象可以从它移动)。

如上所述,我还希望 monad<T>.bind([](auto){}) 通过复制获取 T,monad<T>.bind([](auto &&){}) 通过右值引用获取 T。

正如我所提到的,这是一种 monad<T>.bind() 的重载,根据可调用对象的指定方式会产生不同的效果。如果我们能够像在 lambda 之前那样基于调用签名重载 bind(),那么所有这一切都会很容易。它正在处理捕获 lambda 类型的不可知性,这就是这里的问题。

这应该适用于大多数理智的 lambda(并且通过扩展,非常像 lambda 的东西):

struct template_rref {};
struct template_lref {};
struct template_val {};

struct normal_rref{};
struct normal_lref{};
struct normal_val{};

template<int R> struct rank : rank<R-1> { static_assert(R > 0, ""); };
template<> struct rank<0> {};

template<class F, class A>
struct first_arg {

    using return_type = decltype(std::declval<F>()(std::declval<A>()));
    using arg_type = std::decay_t<A>;


    static template_rref test(return_type (F::*)(arg_type&&), rank<5>);
    static template_lref test(return_type (F::*)(arg_type&), rank<4>);
    static template_lref test(return_type (F::*)(const arg_type&), rank<3>);
    static template_val test(return_type (F::*)(arg_type), rank<6>);

    static template_rref test(return_type (F::*)(arg_type&&) const, rank<5>);
    static template_lref test(return_type (F::*)(arg_type&) const, rank<4>);
    static template_lref test(return_type (F::*)(const arg_type&) const, rank<3>);
    static template_val test(return_type (F::*)(arg_type) const, rank<6>);

    template<class T>
    static normal_rref test(return_type (F::*)(T&&), rank<12>);
    template<class T>
    static normal_lref test(return_type (F::*)(T&), rank<11>);
    template<class T>
    static normal_val test(return_type (F::*)(T), rank<10>);

    template<class T>
    static normal_rref test(return_type (F::*)(T&&) const, rank<12>);
    template<class T>
    static normal_lref test(return_type (F::*)(T&) const, rank<11>);
    template<class T>
    static normal_val test(return_type (F::*)(T) const, rank<10>);

    using result = decltype(test(&F::operator(), rank<20>()));
};

"sane" = 没有像 const auto&&volatile 这样疯狂的东西。

rank 用于帮助管理重载决议 - 选择排名最高的可行重载。

首先考虑作为函数模板的排名靠前的 test 重载。如果 F::operator() 是一个模板,那么第一个参数是一个非推导的上下文(通过 [temp.deduct.call]/p6.1),所以 T 不能被推导,它们被删除来自重载决议。

如果 F::operator() 不是模板,则执行推导,选择适当的重载,并将第一个参数的类型编码为函数的 return 类型。行列有效地建立了 if-else-if 关系:

  • 如果第一个参数是右值引用,对于两个 12 级重载中的一个,推导会成功,所以它被选中;
  • 否则12级重载会推演失败。如果第一个参数是一个左值引用,对于 11 级重载之一的推导将成功,并且选择那个;
  • 否则,第一个参数是按值,10 级重载推导成功。

请注意,我们将排名 10 留在最后,因为无论第一个参数的性质如何,推导总是成功的 - 它可以推导 T 作为引用类型。 (事实上​​ ,如果我们让六个模板重载都具有相同的等级,我们会得到正确的结果,因为部分排序规则,但 IMO 这样更容易理解。)

现在是排名较低的 test 重载,它们具有硬编码的成员函数指针类型作为它们的第一个参数。这些只有在 F::operator() 是模板时才真正发挥作用(如果不是,则排名较高的重载将占上风)。将函数模板的地址传递给这些函数会导致对该函数模板执行模板参数推导,以获得与参数类型匹配的函数类型(参见 [over.over])。

我们考虑 [](auto){}[](auto&){}[](const auto&){}[](auto&&){} 情况。行列编码逻辑如下:

  • 如果函数模板可以实例化为非引用arg_type,那么一定是(auto)(等级6);
  • 否则,如果函数模板可以实例化为采用右值引用类型的东西 arg_type&&,那么它必须是 (auto&&)(等级 5);
  • 否则,如果函数模板可以实例化为采用非 const 限定的东西 arg_type&,那么它必须是 (auto&)(等级 4);
  • 否则,如果函数模板可以实例化为带 const arg_type& 的东西,那么它必须是 (const auto&)(等级 3)。

在这里,我们再次首先处理 (auto) 情况,否则它也可以被实例化以形成其他三个签名。此外,我们在 (auto&) 案例之前处理 (auto&&) 案例,因为对于此推导,转发引用规则适用,并且 auto&& 可以从 arg_type& 推导出来。