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

时间:2015-06-12 11:51:50

标签: c++ c++11 lambda c++14 template-meta-programming

我一直在尝试编写一个特征,该特征可以确定某些可调用函数是否将右值引用作为其第一个参数。这使得一些元编程可以调整在调用callable由外部代码提供的callable时是否使用移动或复制语义(实际上是在用户提供的可调用类型上进行重载)。

#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;
}

上面显然不起作用,但它有希望解释我在寻找什么:我想检测限制其参数的callables只是一个右值参考。

请注意,其他Stack Overflow答案(例如Get lambda parameter type)在这里没有用,因为我需要支持C ++ 14通用lambdas(即采用自动参数的lambdas),因此根据调用运算符的地址进行操作lambda类型内部将因无法解决过载而失败。

您将注意到is_callable_working采用Arg类型,并且可以通过F(Arg)找到可调用F的正确重载。我想要检测的是F(Arg)的可用重载是F::operator()(Arg &&)还是F::operator()(&lt; Arg&gt; )的任何其他引用类型。我想如果F()的模糊重载可用,例如F(Arg)F(Arg &&)都会导致编译器出错,但[](auto)的{​​{1}}不应该不明确。

编辑:希望澄清我的问题。我真的在问C ++元编程能否检测到参数的约束。

编辑2:以下是一些更多说明。我确切的用例是:

[](auto &&)

其中template<class T> class monad { ... template<class U> monad<...> bind(U &&v); }; 需要monad<T>.bind([](T}{})副本,我希望T通过右值引用获取monad<T>.bind([](T &&){})(即可调用者可以从中移动)。

如上所述,我还希望T以副本的形式获取T,并monad<T>.bind([](auto){})将R视为右值参考。

正如我所提到的,这是monad<T>.bind([](auto &&){})的一种重载,根据指定callable的方式会产生不同的效果。如果一个人能够在lambdas之前根据呼叫签名重载monad<T>.bind(),那么这一切都很容易。它正在处理捕获lambda类型的不可知性,这就是问题所在。

1 个答案:

答案 0 :(得分:7)

这应该适用于大多数理智的lambda(并且通过扩展,就像lambdas一样):

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>()));
};

&#34;理智&#34; =没有const auto&&volatile等疯狂的东西。

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

首先考虑作为功能模板的高度排名的test重载。如果F::operator()是模板,则第一个参数是非推导的上下文(通过[temp.deduct.call] /p6.1),因此无法推导出T,并且它们被删除从重载决议。

如果F::operator()不是模板,则执行演绎,选择适当的重载,并在函数的返回类型中编码第一个参数的类型。这些等级有效地建立了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&推断出来。