我正在尝试在C ++中实现Haskell函数foldr
:
template <typename F, typename ForwardIter, typename B>
B foldr(F f, B z0, ForwardIter begin, ForwardIter end)
{
if (begin == end) {
return z0;
} else {
auto lval = *begin;
return f(lval, foldr(f, z0, ++begin, end));
}
}
我的问题是。我可以使用F
中的信息将std::function
类型推断为ForwardIter
吗?
虚构类型:F = std::function<B(typename ForwardIter::value, B)>
考虑到Haskell签名(a->b->b)->b->[a]->b
表示参数之间的关系,我想知道是否有一种方法可以在C ++中指定这种关系。因此,当我顺便将错误的仿函数传递给第一个参数时,编译器会告诉我她需要一个具有精确签名的仿函数。
答案 0 :(得分:6)
考虑到Haskell签名
(a->b->b)->b->[a]->b
表示参数之间的关系,我想知道是否有一种方法可以在C ++中指定这种关系。
是的,有。但是,你误以为人们会用std::function
做到这一点。 std::function
是具有已知签名的可调用对象的容器。为什么要将容器中的东西包装起来作为参数?
您可以简单地使用尾随返回类型,例如:
template<typename F,
typename ForwardIter,
typename B,
typename ValueType = typename std::iterator_traits<ForwardIter>::value_type>
auto foldr(F f, B z0, ForwardIter begin, ForwardIter end)
-> decltype(std::declval<F&>()(std::declval<ValueType&>(), std::declval<B&>())) { ...
您还可以为您的需求定义特征(即F
可以使用迭代器的value_type
和B
类型的另一个参数调用,并返回B
}),并使用该特征拒绝其他callables。
template<typename F,
typename ForwardIter,
typename B>
B foldr(F f, B z0, ForwardIter begin, ForwardIter end)
{
using ValueType = typename std::iterator_traits<ForwardIter>::value_type;
static_assert(is_callable<F&, B(ValueType&, B&)>(), "F must be callable with signature B(B, B)");
...
如果对象无法使用正确的签名进行调用,则会导致错误。如果您只希望从重载决策中排除重载,则可以将SFINAE与enable_if
template<typename F,
typename ForwardIter,
typename B,
typename ValueType = typename std::iterator_traits<ForwardIter>::value_type,
typename std::enable_if<is_callable<F&, B(ValueType&,B&)>::value, int>::type = 0>
B foldr(F f, B z0, ForwardIter begin, ForwardIter end) { ...
或者您可以使用该特征进行标签分派。管他呢。重要的是,这不是std::function
所做的(并且它不仅仅是运行时开销的问题;它还使某些代码在没有std::function
痴迷的情况下工作正常时无法编译)。
所有这一切,如果我正在编写这个函数,我会或多或少地开始这样做(注意所有的引用语义和转发):
template<typename F,
typename It,
typename B,
typename Reference = typename std::iterator_traits<It>::reference,
typename std::enable_if<is_callable<F&, B(Reference&&,B)>::value, int>::type = 0>
B foldr(F&& f, B&& z0, It first, It last)
{
if (first == end) {
return std::forward<B>(z0);
} else {
auto&& r = *first; // must do here to avoid order of evaluation woes
// don't forward f because we cannot "destroy" it twice, i.e. no moving!
return f(std::forward<decltype(r)>(r), foldr(f, std::forward<B>(z0), ++first, end));
}
}
答案 1 :(得分:2)
使用decltype
关键字。在代码中使用auto
表明您使用的是C ++ 11。在你的功能的顶部:
typedef decltype(*begin) ForwardIterValue; // type of result of applying dereference operator
typedef std::function< B(ForwardIterValue, B) > F; // desired function object
正如wikipedia entry所说,
它的主要用途是通用编程,通常是编程 表达依赖的类型是困难的,甚至是不可能的 模板参数
根据您构建函数对象的方式,也可以使用auto
。
答案 2 :(得分:2)
是的,这是可能的。优点是,如果您使用许多不同的函数重复调用foldr
,但ForwardIterator
和B
的类型相同,则只能获得生成代码的一个版本,因此(可能) )一个较小的可执行文件。
但是,我的直觉说“不要这样做”。正是由于std::function
的通用属性,编译器几乎没有机会内联f()
的调用;反过来,它可能难以将递归调用展开到foldr
。另一方面,如果您允许它为每个函数合成一个专门的foldr
,那么您将有更好的机会获得高度优化的实现。如果f
很简单,例如std::plus
或类似的东西,则尤其如此。
例如,在-03时使用Clang 3.1尝试以下操作:
std::vector<int> v{1, 2, 3, 4};
std::cout << foldr(std::plus{}, 5, v.begin(), v.end()) << std::endl;
导致foldr
被完全取消的呼叫。使用std::function
的相同代码的版本具有更多的开销。
当然,本能和直觉是决定是否做某事的特别糟糕的方式,当涉及到C ++模板时,这种方式会变得更加复杂。我建议尝试两种方法,看看哪种方法更好。
答案 3 :(得分:0)
如果
,您可以使用typename std::iterator_traits<ForwardIter>::value_type
typedef ... value_type;
,或 iterator_traits
的适当专业化(例如,用于指针)。