12

I am learning functional programming in C++. My intention is to pass a non generic function as argument. I know about the template method, however I would like to restrict the function signature as part of the API design. I worked out 4 different methods example on cpp.sh:

// Example program
#include <iostream>
#include <string>
#include <functional>

typedef int(functor_type)(int);


int by_forwarding(functor_type &&x)  {
    return x(1);
}

int functor_by_value(functor_type x) {
    return x(1);
}

int std_func_by_value(std::function<functor_type> x) {
    return x(1);
}

int std_func_by_forwarding(std::function<functor_type> &&x) {
    return x(1);
}

int main()
{
    std::cout << functor_by_value([](int a){return a;}); // works
    std::cout << std_func_by_value([](int a){return a;}); // works
    std::cout << std_func_by_forwarding(std::move([](int a){return a;})); // works

    //std::cout << by_forwarding([](int a){return a;}); // how to move lambda with forwarding ?
}

Is any of the above attempts correct? If not, how do i achieve my goal?

Afoxinabox
  • 319
  • 2
  • 9
  • `prog.cc:29:41: warning: moving a temporary object prevents copy elision ` :) clang warnings ftw! – hellow Aug 16 '18 at 09:13
  • 5
    Short answer: `int functor_by_value(functor_type x)`. – George Aug 16 '18 at 09:15
  • `std::invoke` if you have access to C++17 – Madden Aug 16 '18 at 09:17
  • Lambdas are anonymous, therefore you can only grab its type through type deduction. – Passer By Aug 16 '18 at 09:18
  • It depends on meaning you put into "pass a non generic function" First two variants accept a reference or a pointer to function. 3rd and 4th variant accept an object or an rvalue reference to object with overloaded `operator ()`. So every `x` has an `operator ()` so usual calling syntax `x(1);` works, however none of them are functions. – user7860670 Aug 16 '18 at 09:20
  • What do you mean by a "non generic" function? There are no generic functions in C++ in any sense of "generic" that I'm aware of (although some might refer to function templates as generic functions). From the examples, it seems like you're referring to lambdas. – molbdnilo Aug 16 '18 at 09:33
  • By "non generic" I mean a function that has a type restriction. In my example it is `int(int)`. This type shall be defined on the API side. A practical application would be to accept a handler callback function `f(SpecificResponseType handle)`. To my understanding, if I use Jarod42's definition (in his answer), I could pass any function, resulting in a runtime error. – Afoxinabox Aug 16 '18 at 09:39
  • As a side note. 1) In `int by_forwarding(functor_type &&x)` `functor_type&&` is not a forwarding reference it is a rvalue refence, so "by forwarding" part is wrong. 2) You should not use `std::move` on temporaries. You probably should read about "`std::move` being a cast and not moving anything" – Serikov Aug 16 '18 at 12:04

4 Answers4

14

(based on clarification from comments)

Signature can be restricted by using std::is_invocable:

template<typename x_Action> auto
functor_by_value(x_Action && action)
{
    static_assert(std::is_invocable_r_v<int, x_Action, int>);
    return action(1);
}

online compiler

user7860670
  • 35,849
  • 4
  • 58
  • 84
5

Other alternative:

template <typename Func>
auto functor_by_value(Func&& f)
-> decltype(std::forward<Func>(f)(1))
{
    return std::forward<Func>(f)(1);
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
5

As usual, this depends on how good your compiler is today, and how good it will be in the future.

Currently, compilers are not very good at optimizing std::function. Surprisingly, std::function is a complicated object that sometimes has to allocate memory to maintain stateful lambda functions. It also complicates matters that std::function has to be able to refer to member function, regular functions, and lambdas, and do it in a transparent manner. This transparency has a hefty runtime cost.

So, if you want the fastest possible code, you should be careful with std::function. For that reason the first variant is the fastest (on today's compilers):

int functor_by_value(functor_type x) {
    return x(1);
}

It simply passes a pointer to a function.

When stateful lambdas are involved you have only two options. Either pass the lambda as a template argument, or convert to std::function. Hence, if you want the fastest code possible with lambdas (in today's compilers), you'd pass the function as a templated argument.

Since a lambda function may have a big state, passing it around may copy the big state (when copy elision is not possible). GCC will construct the lambda directly on the parameter list (with no copy), but a nested function will invoke a copy constructor for the lambda. To avoid that, either pass it by const reference (in that case it can't be mutable), or by rvalue reference:

template<class Func>
void run2(const Func & f)
{
    std::cout << "Running\n";
    f();
}
template<class Func>
void run(const Func & f)
{
    run2(f);
}
int main()
{
    run([s=BigState()]() { std::cout << "apply\n"; });
    return 0;
}

Or:

template<class Func>
void run2(Func && f)
{
    f();
}
template<class Func>
void run(Func && f)
{
    run2(std::forward<Func>(f));
}
int main()
{
    run([s=BigState()]() { std::cout << "apply\n"; });
    return 0;
}

Without using references, the BigState() will be copied when the lambda is copied.

UPDATE: After reading the question again I see that it wants to restrict the signature

template<typename Func, 
         typename = std::enable_if_t<
            std::is_convertible_v<decltype(Func(1)), int>>>
void run2(const Func & f)
{
    std::cout << "Running\n";
    f();
}

This will restrict it to any function that can accept int (possibly with an implicit cast), and returns an int or any type that is implicitly cast to int. However, if you want to accept only function-like objects that accept exactly int and return exactly int you can see if the lambda is convertible to std::function<int(int)>

Michael Veksler
  • 8,217
  • 1
  • 20
  • 33
5

however I would like to restrict the function signature as part of the API design.

So restrict it:

#include <functional>
#include <type_traits>
#include <iostream>

/// @tparam F is a type which is callable, accepting an int and returning an int
template
<
    class F, 
    std::enable_if_t
    <
        std::is_convertible_v<F, std::function<int(int)>>
    >* = nullptr
>
int myfunc(F &&x) {
    return x(1);
}

int main()
{
    auto a = myfunc([](int x) { std::cout << x << std::endl; return 1; });

    // does not compile
    // auto b = myfunc([]() { std::cout << "foo" << std::endl; return 1; });
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 2
    This would fail when object can be invoked as `int (int)` but can not be converted to `std::function` object. [For example when object is not copyable / movable but has `public: int operator ()(int)`](https://wandbox.org/permlink/ktOE29JgZyMtSdAM). – user7860670 Aug 16 '18 at 10:00
  • @VTT I do agree - std::function expects function objects to be callable on the const interface, which may or may not be a bug in the standard, depending on how you look at it. However, the OP mentioned that std::function was a candidate for the argument type, so I took a lead from there. I saw your answer (and upvoted). I was not aware of the is_invocable_r_t type trait. – Richard Hodges Aug 16 '18 at 11:24