【问题标题】:c++11: How to write a wrapper function to make `std::function` objectsc++11:如何编写一个包装函数来制作`std::function`对象
【发布时间】:2014-02-12 20:21:15
【问题描述】:

我正在尝试编写一个包装器make_function,它像std::make_pair 一样可以从合适的可调用对象中创建一个std::function 对象。

就像make_pair,对于函数指针fooauto f0 = make_function(foo); 创建一个正确类型签名的std::function 函数对象f0。 澄清一下,我不介意偶尔给make_function 提供类型参数,以防很难(或不可能)从参数中完全推断出类型。

到目前为止,我想出的东西(下面的代码)适用于 lambdas、一些函数指针和仿函数(我没有考虑 volatiles)。但我无法让它适用于std::bindstd::bind<R> 结果。在下面的代码中

auto f2 = make_function(std::bind(foo,_1,_2,_3)); //not OK

使用 gcc 4.8.1 无法编译/工作。我猜我没有正确捕获operator()bind 结果,但我不确定如何修复它。

感谢任何有关如何解决此案例或改进其他极端案例的帮助。

我的问题当然是如何修复下面示例中的错误。

作为背景,我使用这个包装器的一个案例可以在这个问题上找到:How to make C++11 functions taking function<> parameters accept lambdas automatically。如果您不同意使用std::function 或我的具体使用方式,请将您的 cmets 留在该帖子中,并在此处讨论技术问题。

--- 编辑---

从一些 cmets 中,我了解到这是因为歧义问题(std::bind 结果的函数调用 operator() 的歧义)。正如@Mooing Duck 的回答所指出的,解决方案是明确给出参数类型。我更新了代码以结合@Mooing Duck 答案中的三个函数(类型参数略有变化),以便make_function 包装器现在可以像以前一样处理/类型推断明确的情况,并允许指定完整的类型签名当有歧义时。

(我的明确案例的原始代码位于:https://stackoverflow.com/a/21665705/683218,可以在:https://ideone.com/UhAk91 进行测试):

#include <functional>
#include <utility>
#include <iostream>
#include <functional>
using namespace std;

// For generic types that are functors, delegate to its 'operator()'
template <typename T>
struct function_traits
  : public function_traits<decltype(&T::operator())>
{};

// for pointers to member function
template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const> {
  enum { arity = sizeof...(Args) };
  typedef function<ReturnType (Args...)> f_type;
};

// for pointers to member function
template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) > {
  enum { arity = sizeof...(Args) };
  typedef function<ReturnType (Args...)> f_type;
};

// for function pointers
template <typename ReturnType, typename... Args>
struct function_traits<ReturnType (*)(Args...)>  {
  enum { arity = sizeof...(Args) };
  typedef function<ReturnType (Args...)> f_type;
};

template <typename L> 
static typename function_traits<L>::f_type make_function(L l){
  return (typename function_traits<L>::f_type)(l);
}

//handles bind & multiple function call operator()'s
template<typename ReturnType, typename... Args, class T>
auto make_function(T&& t) 
  -> std::function<decltype(ReturnType(t(std::declval<Args>()...)))(Args...)> 
{return {std::forward<T>(t)};}

//handles explicit overloads
template<typename ReturnType, typename... Args>
auto make_function(ReturnType(*p)(Args...))
    -> std::function<ReturnType(Args...)> {
  return {p};
}

//handles explicit overloads
template<typename ReturnType, typename... Args, typename ClassType>
auto make_function(ReturnType(ClassType::*p)(Args...)) 
    -> std::function<ReturnType(Args...)> { 
  return {p};
}

// testing
using namespace std::placeholders;

int foo(int x, int y, int z) { return x + y + z;}
int foo1(int x, int y, int z) { return x + y + z;}
float foo1(int x, int y, float z) { return x + y + z;}

int main () {
  //unambuiguous
  auto f0 = make_function(foo);
  auto f1 = make_function([](int x, int y, int z) { return x + y + z;});
  cout << make_function([](int x, int y, int z) { return x + y + z;})(1,2,3) << endl;

  int first = 4;
  auto lambda_state = [=](int y, int z) { return first + y + z;}; //lambda with states
  cout << make_function(lambda_state)(1,2) << endl;

  //ambuiguous cases
  auto f2 = make_function<int,int,int,int>(std::bind(foo,_1,_2,_3)); //bind results has multiple operator() overloads
  cout << f2(1,2,3) << endl;
  auto f3 = make_function<int,int,int,int>(foo1);     //overload1
  auto f4 = make_function<float,int,int,float>(foo1); //overload2

  return 0;
}

Ideone

【问题讨论】:

  • 一开始为什么要这样做? auto f = std::bind(foo, _1, _2); 比同等的 std::function 好!
  • 原因是我需要为函数参数输入fstd::function。并且std::function 类型变量不自动接受bind 结果或其他可调用对象。
  • “std::function 类型变量不接受绑定结果或其他可自动调用的变量”是什么意思:coliru.stacked-crooked.com/a/d66117115e71a7ee
  • 你一直在寻求一些既不可能很好解决的问题(黑客解决方案在 C++1y 中会变得越来越糟糕),而且对于我所拥有的几乎所有实际案例都有错误的解决方案检查它试图解决的地方。它不能解决 lambdas,因为 make_function([](auto x, auto y, auto z) { return x + y + z;}) 失败(欢迎使用 C++1y!)
  • 完成任务很不错,IMO make_intmake_double 怎么样? make_string? make_vector?问题仍然存在:为什么你想要一个推断std::function?除了弄清楚你是否能实现它之外,你还有其他需要吗?您真正要做的是应用类型擦除,并支付相关费用,没有特别的原因。除了脑力练习之外,您的主要目标是编写一个会降低应用程序性能的复杂机制。

标签: c++ c++11 c++14 std-function


【解决方案1】:

问题是您的代码没有正确处理 lambda、bind 或 functionoid,您的代码假定所有这些都没有参数。要处理这些,您必须指定参数类型:

//plain function pointers
template<typename... Args, typename ReturnType>
auto make_function(ReturnType(*p)(Args...))
    -> std::function<ReturnType(Args...)> 
{return {p};}

//nonconst member function pointers
template<typename... Args, typename ReturnType, typename ClassType>
auto make_function(ReturnType(ClassType::*p)(Args...)) 
    -> std::function<ReturnType(Args...)>
{return {p};}

//const member function pointers
template<typename... Args, typename ReturnType, typename ClassType>
auto make_function(ReturnType(ClassType::*p)(Args...) const) 
    -> std::function<ReturnType(Args...)>
{return {p};}

//qualified functionoids
template<typename FirstArg, typename... Args, class T>
auto make_function(T&& t) 
    -> std::function<decltype(t(std::declval<FirstArg>(), std::declval<Args>()...))(FirstArg, Args...)> 
{return {std::forward<T>(t)};}

//unqualified functionoids try to deduce the signature of `T::operator()` and use that.
template<class T>
auto make_function(T&& t) 
    -> decltype(make_function(&std::remove_reference<T>::type::operator())) 
{return {std::forward<T>(t)};}

变量:

int func(int x, int y, int z) { return x + y + z;}
int overloaded(char x, int y, int z) { return x + y + z;}
int overloaded(int x, int y, int z) { return x + y + z;}
struct functionoid {
    int operator()(int x, int y, int z) { return x + y + z;}
};
struct functionoid_overload {
    int operator()(int x, int y, int z) { return x + y + z;}
    int operator()(char x, int y, int z) { return x + y + z;}
};
int first = 0;
auto lambda = [](int x, int y, int z) { return x + y + z;};
auto lambda_state = [=](int x, int y, int z) { return x + y + z + first;};
auto bound = std::bind(func,_1,_2,_3);

测试:

std::function<int(int,int,int)> f0 = make_function(func); assert(f0(1,2,3)==6);
std::function<int(char,int,int)> f1 = make_function<char,int,int>(overloaded); assert(f1(1,2,3)==6);
std::function<int(int,int,int)> f2 = make_function<int,int,int>(overloaded); assert(f2(1,2,3)==6);
std::function<int(int,int,int)> f3 = make_function(lambda); assert(f3(1,2,3)==6);
std::function<int(int,int,int)> f4 = make_function(lambda_state); assert(f4(1,2,3)==6);
std::function<int(int,int,int)> f5 = make_function<int,int,int>(bound); assert(f5(1,2,3)==6);
std::function<int(int,int,int)> f6 = make_function(functionoid{}); assert(f6(1,2,3)==6);
std::function<int(int,int,int)> f7 = make_function<int,int,int>(functionoid_overload{}); assert(f7(1,2,3)==6);
std::function<int(char,int,int)> f8 = make_function<char,int,int>(functionoid_overload{}); assert(f8(1,2,3)==6);

http://coliru.stacked-crooked.com/a/a9e0ad2a2da0bf1f 您的 lambda 成功的唯一原因是它可以隐式转换为函数指针,因为您的示例没有捕获任何状态。请注意,我的代码需要重载函数的参数类型,带有重载 operator()(包括绑定)的 functionoid,但现在能够推断出所有非重载的 functionoid。

decltype 行很复杂,但它们用于推断返回类型。请注意,在我的所有测试中都不需要指定返回类型。让我们分解make_function&lt;short,int,int&gt;,好像Tchar(*)(short, int, int)

-> decltype(t(std::declval<FirstArg>(), std::declval<Args>()...))(FirstArg, Args...)
`std::declval<FirstArg>()` is `short{}` (roughly)
-> decltype(t(short{}, std::declval<Args>()...))(FirstArg, Args...)
`std::declval<Args>()...` are `int{}, int{}` (roughly)
-> decltype(t(short{}, int{}, int{})(FirstArg, Args...)
`t(short{}, int{}, int{})` is an `int{}` (roughly)
-> decltype(short{})(FirstArg, Args...)
`decltype(int{})` is `int`
-> int(FirstArg, Args...)
`FirstArg` is still `short`
-> int(short, Args...)
`Args...` are `int, int`
-> int(short, int, int)
So this complex expression merely figures out the function's signature
well, that should look familiar...

【讨论】:

  • 我试过了:auto v = make_function([](int x, int y, int z) { return x + y + z;})(1,2,3);,好像没问题。
  • @TingL:在那里,我发现了带有状态、重载函数和重载成员函数的 lambdas
  • 感谢您的帮助。我添加了您的 lambda_state 测试并进行了尝试,但 lambda_state 似乎与 OP 中的代码可以正常工作。请查看更新代码。我正在使用 mingw32 的 gcc4.8.1。
  • 介意解释一下-&gt; std::function&lt;decltype(std::declval&lt;T&gt;()(std::declval&lt;Args&gt;()...))(Args...)&gt; 吗?这可能是我错过的。这是否也捕获了某些operator()
  • @TingL:它将捕获任何具有任意数量参数的函数。不幸的是,要使用该版本,您还必须指定参数的类型,以便它可以确定要使用的重载。
【解决方案2】:

一般来说,如果没有严格的限制,您将无法解决它,即您传递给 make_function 的任何内容都只能通过一个签名来调用。

你打算如何处理类似的事情:

struct Generic
{
    void operator()() { /* ... */ }
    void operator()() const { /* ... */ }

    template<typename T, typename... Ts>
    T operator()(T&& t, Ts&&...) { /* ... */ }

    template<typename T, typename... Ts>
    T operator()(T&& t, Ts&&...) const { /* ... */ }
};

C++14 通用 lambda 也会有同样的问题。

std::function 中的签名基于您计划如何调用它,而不是基于您如何构造/分配它。

std::bind 也无法解决它,因为它具有不确定的数量:

void foo() { std::cout << "foo()" << std::endl; }
//...

auto f = std::bind(foo);
f();                 // writes "foo()"
f(1);                // writes "foo()"
f(1, 2, 3, 4, 5, 6); // writes "foo()"

【讨论】:

  • 感谢您的 cmets。如果有歧义,是否可以在make_function中具体指定result_type和args类型?目前,我想把基础案例做好,其他的留给以后的工作。
  • @TingL: auto f = std::make_function&lt;void,int&gt;(Generic())std::function&lt;void,int&gt; f = Generic 好多少?
  • ... 或 auto f = std::function&lt;void (int)&gt;(Generic());
  • 是的,在最坏的情况下,可能也好不了多少。但是在许多可以推断类型参数的普通用例中(例如当前的非泛型 lambda),它要好得多。
  • @Nevin,您的示例 auto f = std::bind(foo); 在我的情况下无法编译。
【解决方案3】:

您希望能够将 lambda 转换为 std::function 的主要原因是您需要两个重载,每个重载都采用不同的签名。

解决这个问题的一个好方法是std::result_of

假设您正在制作一个采用 lambda 或其他函数的循环控制结构。如果该函数返回 void,则您希望不受控制地循环。如果它返回bool 或类似的,你想在它返回true 时循环。如果它返回enum ControlFlow,你要注意ControlFlow的返回值(比如continuebreak)。有问题的函数要么接受元素迭代,要么接受一些额外的数据(迭代中的索引,可能是关于元素的一些“位置”元信息等)。

std::result_of 会让您假装使用不同数量的参数调用传入的类型。然后,特征类可以确定上述签名中的哪个是“最佳匹配”,然后路由到处理该签名的实现(可能通过将“更简单”的情况包装在 lambda 中并调用更复杂的情况)。

天真地,您的make_function 可能会出现这个问题,因为您可以简单地在各种std::function&lt; blah(etc) &gt; 案例上重载。但是随着auto 参数的传递,std::bind 已经进行了完美的转发,这只处理最简单的情况。

std::result_of 特征类(以及可能相关的概念匹配和requires 子句)和标签调度(或 SFINAE 作为最后的手段)。

最大的缺点是您最终不得不自己半手动管理覆盖命令。我可以在帮助类中看到一些实用程序,您可以在其中提供要匹配的签名列表,它会生成boost::variant,或者您还可以生成规范输出和到该规范输出的转换方法。

简短的回答? std::bind 的实现是特定于实现的细节,但它可能涉及等价于可变参数包的完美转发,因此不适合您的“获取唯一具体 operator() 的地址”技术使用。

再举一个例子:

template <typename A,typename B> 
vector<B> map(std::function<B (A)> f, vector<A> arr) {
   vector<B> res;
   for (int i=0;i<arr.size();i++) res.push_back(f(arr[i]));
   return res;
}

应该写成:

template<typename expression>
using result = typename std::result_of<expression>::type;
template<typename expression>
using decayed_result = typename std::decay<result<expression>>::type;

template <typename function,typename B> 
vector<decayed_result<function(B)>> map(function&& f, vector<A> const& arr) {
   vector<decayed_result<function(B)>> res;
   res.reserve( arr.size() );
   for (A const& a : arr) {
     res.push_back( f(a) );
   }
   return res;
}

同样,result_of 是正确的解决方案,而不是不必要地将内容转换为 std::function

对于fold_right,我们得到:

template<bool b, typename T=void>
using EnableIf = typename std::enable_if<b,T>::type;

template<typename function, typename src, typename dest>
EnableIf<
  std::is_convertible< result<function(src, dest)>, dest >::value,
  std::vector<dest>
>
fold_right( function&& f, std::vector<src> const& v, dest initial )

再次跳过f 上的任何类型擦除。如果你真的想在f 上进行类型擦除,你可以这样做:

template<typename T> struct identity { typedef T type; };
template<typename T> using do_not_deduce = typename identity<T>::type;

template<typename src, typename dest>
std::vector<dest> fold_right( do_not_deduce< std::function<dest(src,dest)> > f, std::vector<src> const& v, dest init );

std::function 是一个类型擦除 对象。您键入 erase 是因为您想在不想将类型转移到的地方使用类型。从类型推断你应该创建什么样的结果类型擦除对象几乎总是错误的答案,因为你有非类型擦除情况的所有依赖,以及类型擦除的所有低效率。

make_function 的结果取决于源的完整类型,这使得类型擦除输出几乎完全无用。

【讨论】:

  • 我认为您误解了我将 lambda 转换为 std::function 的原因。至少你的解释和我的想法或写的完全不同。
  • 除此之外,感谢您指出这个实现问题。我希望std::bind。希望有人能够对技术问题给出一个很长的答案。
  • @TingL 为我提供一个实际问题,键入推断您要转换为的 std::function 解决的问题。我敢肯定那里可能有一个,但老实说我还没有遇到过。我添加了一个解决问题的方法,该解决方案激发了您在我的旧帖子中的灵感,再次取代了推断std::function&lt;A(B)&gt; 类型的任何需要。
  • 请给出具体链接。再说一次,我没有时间就用例进行另一轮讨论。正如我在问题中所说,请参阅我关于使用的另一篇文章。如果你不喜欢它,在那里批评我。
  • 这对你来说可能不实用,我尊重这一点。我在自己的编程中使用它。这对我来说已经足够了。我希望你能容忍它以及我没有时间解释我的代码是做什么的这一事实。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-12-15
  • 1970-01-01
  • 1970-01-01
  • 2023-04-05
  • 1970-01-01
  • 2018-04-17
  • 1970-01-01
相关资源
最近更新 更多