18

I stumbled across this during my experiments with C++11. I find that it is an obvious solution, but I haven't been able to find any other examples of it in the wild, so I'm concerned that there's something I'm missing.

The practice I'm referring to (in the "addAsync" function):

#include <thread>
#include <future>
#include <iostream>
#include <chrono>

int addTwoNumbers(int a, int b) {
    std::cout << "Thread ID: " << std::this_thread::get_id() << std::endl;

    return a + b;
}

void printNum(std::future<int> future) {
    std::cout << future.get() << std::endl;
}

void addAsync(int a, int b, auto callback(std::future<int>) -> void) { //<- the notation in question
    auto res = std::async(std::launch::async, addTwoNumbers, a, b);

    if (callback) //super straightforward nullptr handling
        return callback(std::move(res));
}

int main(int argc, char** argv) {
    addAsync(10, 10, [](std::future<int> number) { //lambda functions work great
        addAsync(number.get(), 20, [](std::future<int> number) {
            addAsync(893, 4387, printNum); //as do standard functions
            addAsync(2342, 342, nullptr); //executes, sans callback

            std::cout << number.get() << std::endl;
        });
    });

    std::cout << "main thread: " << std::this_thread::get_id() << std::endl;

    return 0;
}

Is it considered bad practice, or is it non-portable (I've only tried it in MSVC++ 2015)? Also, how does the compiler treat this; by conversion to std::function?

I would love to keep using this in my projects, as it obviously states the required argument types and return type in the "signature", accepts a nullptr for optionality, and seems to "just work" (I am aware that these are famous last words in C++).

Hunter Mueller
  • 223
  • 2
  • 9
  • 2
    @Amadeus: Maybe it's the sentences that end in question marks. There are many il-formed questions on SO; this is *not* one of them. – Nicol Bolas Aug 22 '16 at 19:20
  • 3
    The parameter is declared to have a function type `void (std::future)`, which is adjusted to a pointer to function `void (*)(std::future)`. – T.C. Aug 22 '16 at 19:36

3 Answers3

24

auto callback(std::future<int>) -> void is the declaration of a entity of type void(std::future<int>) called callback. When listed as an argument, the compiler adjusts this to be a pointer-to-function of type void(*)(std::future<int>).

Your lambda is stateless, and as such can be implicitly converted to a function pointer.

Once you add a non-trivial capture, your code will stop compiling:

[argc](std::future<int> number) { 
   std::cout << argc << '\n';

...

Now, ignoring your question content and looking at the title...

There is a modest cost for a std::function because it is a value-type, not a view-type. As a value-type, it actually copies its argument.

You can get around this by wrapping the calling object in a std::ref, but if you want to state "I won't keep this function object around longer than this call", you can write a function_view type as follows:

template<class Sig>
struct function_view;

template<class R, class...Args>
struct function_view<R(Args...)> {
  void* ptr = nullptr;
  R(*pf)(void*, Args...) = nullptr;

  template<class F>
  using pF = decltype(std::addressof( std::declval<F&>() ));

  template<class F>
  void bind_to( F& f ) {
    ptr = (void*)std::addressof(f);
    pf = [](void* ptr, Args... args)->R{
      return (*(pF<F>)ptr)(std::forward<Args>(args)...);
    };
  }
  // when binding to a function pointer
  // even a not identical one, check for
  // null.  In addition, we can remove a
  // layer of indirection and store the function
  // pointer directly in the `void*`.
  template<class R_in, class...Args_in>
  void bind_to( R_in(*f)(Args_in...) ) {
    using F = decltype(f);
    if (!f) return bind_to(nullptr);
    ptr = (void*)f;
    pf = [](void* ptr, Args... args)->R{
      return (F(ptr))(std::forward<Args>(args)...);
    };
  }
  // binding to nothing:
  void bind_to( std::nullptr_t ) {
    ptr = nullptr;
    pf = nullptr;
  }       
  explicit operator bool()const{return pf;}

  function_view()=default;
  function_view(function_view const&)=default;
  function_view& operator=(function_view const&)=default;

  template<class F,
    std::enable_if_t< !std::is_same<function_view, std::decay_t<F>>{}, int > =0,
    std::enable_if_t< std::is_convertible< std::result_of_t< F&(Args...) >, R >{}, int> = 0
  >
  function_view( F&& f ) {
    bind_to(f); // not forward
  }

  function_view( std::nullptr_t ) {}

  R operator()(Args...args) const {
      return pf(ptr, std::forward<Args>(args)...);
  }
};

live example.

This is also useful in that it is a strictly simpler kind of type erasure than std::function, so it could be educational to go over it.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thanks for expanding on aschepler's answer. Another example of compiler handholding. – Hunter Mueller Aug 22 '16 at 20:41
  • 3
    s/variable/entity/. There are no variables of function type. – T.C. Aug 22 '16 at 22:49
  • @nik-z not in signature of `operator()`, that should match explicit type passed to`function_view`. Possibly in `pf` yes. `decltype(auto)` goea nowhere. – Yakk - Adam Nevraumont Sep 23 '18 at 00:19
  • 1
    @Yakk-AdamNevraumont This is pretty cool. It won't work when you're using a member function though, say `class Foo{public:int _num = 100; void print_add(int i) { cout << _num + i;}};`. Test it with: `Foo foo{}; function_view fr = &Foo::print_add; fr(foo,10);` Doesn't work. gcc outputs: `must use '.*' or '->*' to call pointer-to-member function in '*(function_view::pF)ptr (...)', e.g. '(... ->* *(function_view::pF)ptr) (...)'`. – KeyC0de Sep 22 '19 at 13:10
  • @Yakk-AdamNevraumont However if i replace exactly `function_view` with `std::function` it will work and print `110` as it's supposed to. So this is not a complete replacement for `std::function`. – KeyC0de Sep 22 '19 at 13:10
  • @nikos yes; when I wrote the above `std::invoke` wasn't part of the standard yet. A few changes should make it work; and in C++14 you could probably use the std ref trick. – Yakk - Adam Nevraumont Sep 22 '19 at 15:25
  • @Yakk-AdamNevraumont I suppose with the conversion from the method* we get from the template type to void* and then back to function* (since it is illegal to convert directly from method* to function*). May need a little bit more template tricks to do it. I don't think `std::ref` will work. Haven't tested it though. – KeyC0de Sep 22 '19 at 15:50
  • @nikos also meyhod ptr cannot.convert to void ptr. – Yakk - Adam Nevraumont Sep 22 '19 at 16:25
  • @Yakk-AdamNevraumont It can. – KeyC0de Sep 22 '19 at 16:28
  • @nikos no, the standard does not require that to work. In particular, member pointers to virtual meothods are sometimes too large, iirc. – Yakk - Adam Nevraumont Sep 22 '19 at 16:52
  • @Yakk-AdamNevraumont I'm not sure about virtual methods, never tried it. But it is possible with a little casting trickery to convert non-virtual method pointers to void* and then to function pointers (with or without cv qualifiers to the member function etc - they all work). I could show you, but we need more space. – KeyC0de Sep 22 '19 at 16:55
  • @nikos "it works" is not the same as "it is allowed". https://stackoverflow.com/questions/1307278/casting-between-void-and-a-pointer-to-member-function – Yakk - Adam Nevraumont Sep 22 '19 at 16:59
  • @Yakk-AdamNevraumont Well in that sense converting `function*` to `void*` (and vice versa) shouldn't be valid either, but still compilers allow it, but in some architectures this is simply incompatibly impossible, ie it would cause real hardware bugs. – KeyC0de Sep 22 '19 at 17:02
  • @nikos yes, so I need to replace the `void*` with a `union{ void* ptr; void(*fun)(); void(X::*mfun)(); }`; pass that (by-value) to the lambda, then grab the one I know I wrote to. And on MSVC, I have to make sure the type `X` is sufficient, as MSVC violates the standard and has two sizes of member function pointers (!); alternatively, just store `aligned_storage_t< std::max( sizeof(void*), sizeof(void(*)()), sizeof(void(X::*)()), alignof... >` around. – Yakk - Adam Nevraumont Sep 22 '19 at 18:27
  • That could work. I haven't used unions myself. Hold up, I'll post how soon. – KeyC0de Sep 22 '19 at 22:18
16

You are using a raw pointer to function.

Unlike std::function, this will not work with a lambda that captures, or with a result of std::bind, or with a general class type that implements operator().

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Ah, I was so hoping it wouldn't end up being a raw pointer. Thanks very much for clarifying. The limitations re: captures are a deal-breaker in many cases, and for consistency's sake I think I'll stick with std::function and std::bind. I'll mark this as the answer! – Hunter Mueller Aug 22 '16 at 20:38
3

"Alternative to std::function for passing function as argument"

One alternative would be a function pointer (including member function pointer). But std::function is just so much nicer (IMO).

Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70
  • Yes, it's nices but std::function has a overhead that pointer to function doesn't have. Most of the time this overhead can be ignored, but when working with tasks that require optimization it can be problematic. – JFValdes Dec 12 '19 at 12:56