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

Nia*_*las 5 c++ lambda template-meta-programming c++11 c++14

我一直试图写一个特征来判断一些可调用是否将右值引用作为其第一个参数.这使得一些元编程调整在调用可调用区域时是否使用移动或复制语义,其中可调用区域由外部代码提供(实际上一个是在用户提供的可调用类型上过载).

#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;
}
Run Code Online (Sandbox Code Playgroud)

上面显然不起作用,但它有希望解释我在寻找:我想检测限制其参数的callables只是一个rvalue引用.

请注意,其他Stack Overflow答案(例如Get lambda参数类型)在这里没有用,因为我需要支持C++ 14泛型lambdas(即采用自动参数的lambdas),因此基于在lambda类型中获取调用运算符的地址进行欺骗将无法解决过载而失败.

您将注意到is_callable_working采用Arg类型,并且可以通过找到可调用F的正确重载F(Arg).我想要检测的是,可用的重载F(Arg)是a F::operator()(Arg &&)还是F::operator()(<Arg的任何其他引用类型> ).我可以想象,如果F()可以获得模糊的重载,例如两者F(Arg),F(Arg &&)那么编译器就会出错,但是[](auto)不应该有歧义[](auto &&).

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

编辑2:这里有一些澄清.我确切的用例是这样的:

template<class T> class monad
{
  ...
  template<class U> monad<...> bind(U &&v);
};
Run Code Online (Sandbox Code Playgroud)

其中monad<T>.bind([](T}{})需要T通过复制,我想monad<T>.bind([](T &&){})需要T通过右值引用(即调用可以从中移动).

如上所述,我也想monad<T>.bind([](auto){})通过副本取T,并monad<T>.bind([](auto &&){})通过右值引用取T.

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

T.C*_*.C. 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>()));
};
Run Code Online (Sandbox Code Playgroud)

"理智"=没有疯狂的东西,如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&.