Reputation: 173
I'm trying to implement the Haskell function foldr
in C++:
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));
}
}
My question is. Can I deduce the type F
to std::function
using info from ForwardIter
?
Imaginary type: F = std::function<B(typename ForwardIter::value, B)>
Considering the Haskell signature (a->b->b)->b->[a]->b
that indicates the relationship between arguments, I'm wondering if there is one way to specify this kind of relationship in C++. So that when I incidentally passing a wrong functor to 1st argument, the compiler would tell me that she need a functor with exact signature.
Upvotes: 2
Views: 288
Reputation: 234514
Considering the Haskell signature
(a->b->b)->b->[a]->b
that indicates the relationship between arguments, I'm wondering if there is one way to specify this kind of relationship in C++.
Yes, there is. You are misguided in thinking that one does that with std::function
, though. std::function
is a container for callables with a known signature. Why would one wrap stuff in a container just for passing it as an argument?
You can simply use a trailing return type, for example:
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&>())) { ...
You can also define a trait for your requirements (i.e. F
is callable with an argument of the value_type
of the iterator and another of typeB
, and returning B
), and use that trait to reject other 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)");
...
This will cause an error if the object is not callable with the right signature. If you only want the overload to be excluded from overload resolution you can use SFINAE with 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) { ...
Or you can use that trait for tag dispatching. Or whatever. The important bit is that this is not what std::function
was made for (and it's not just a problem of runtime overhead; it also makes certain code fail to compile when it would work just fine without the std::function
obsession).
All that said, if I was writing this function, I would start more or less like this (note all the reference semantics and forwarding going on):
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));
}
}
Upvotes: 6
Reputation: 29598
You can use typename std::iterator_traits<ForwardIter>::value_type
if either
typedef ... value_type;
, oriterator_traits
exists (e.g. for pointers).Upvotes: 0
Reputation: 16824
Yes, it would be possible. The advantage is that if you call foldr
repeatedly with many different functions, but the same types for ForwardIterator
and B
, you will only get one version of the generated code, and thus (potentially) a smaller executable.
However, my instinct says "don't do it". Precisely because of the generic properties of std::function
, the compiler has little chance of inlining your call to f()
; in turn, it may have difficulty unwinding the recursive call to foldr
. On the other hand, if you allow it to synthesize a specialized foldr
for each function, you've got a much better chance of getting a highly optimised implementation. This is particularly true if f
is something simple, like std::plus
or something similar.
For example, trying the following with Clang 3.1 at -03:
std::vector<int> v{1, 2, 3, 4};
std::cout << foldr(std::plus{}, 5, v.begin(), v.end()) << std::endl;
leads to the call to foldr
being completely eliminated. A version of the same code using std::function
has much more overhead.
Of course, instinct and gut feeling are particularly terrible ways of deciding whether or not to do something, and that goes double when C++ templates get involved. I would suggest trying both approaches and seeing which turns out better.
Upvotes: 2
Reputation: 4673
Use the decltype
keyword. The use of auto
in your code suggests you are using C++11. At the top of your function:
typedef decltype(*begin) ForwardIterValue; // type of result of applying dereference operator
typedef std::function< B(ForwardIterValue, B) > F; // desired function object
As the wikipedia entry says,
Its primary intended use is in generic programming, where it is often difficult, or even impossible, to express types that depend on template parameters
Depending on how you construct the function object, using auto
for that as well might be possible.
Upvotes: 2