N3579
Revision of N3466=12-0156
2013-03-15
Mike Spertus
mike_spertus@symantec.com

A type trait for signatures

Overview

We propose a (compiler-supported) type trait std::signature whose type typedef is the function type of the callable object when called with the given parameter types.

For example, if we define C as struct C { int operator()(double d, int i); int operator()(double d1, double d2); }; then std::signature<C(int, int)>::type would be int(double, double). More precisely the return type of signature<Fn(ArgTypes...)> is result_of<Fn(ArgTypes...)> and each argument type is the same as the formal parameter type matched in the call to Fn. For more detailed information, see the use cases and wording below.

Example application: More perfect forwarding

This section gives several examples of using std::signature to create transparent forwarding functions. Note that this paper does not propose changing async, etc., but to illustrate, as Pablo Halpern has said, that the problem is real for any "pass-through" functions that need to capture arguments.

The moment a programmer first encounters multithreaded programs, they run into surprises like the following example: // Inspired by examples in section 2.2 of Anthony Williams C++ Concurrency in Action void f(int &i) { i = 2; /* ... */ } int i; f(i); // i passed by reference, f can change i thread tf = thread(f, i); // Ill-formed: i not passed by reference While they can always force a pass by reference,e.g., by thread t(f, ref(i)); this is entirely different from how functions arguments are ordinarily passed. While the above example is at least a compile error, we can even get silent errors introduced for parameters that are passed by value // Another variation on an example in section 2.2 of Anthony Williams C++ Concurrency in Action void f(int i,std::string s); // Unlike in the book, s is passed by value. void my_oops(int some_param) { char buffer[1024]; sprintf(buffer, "%i",some_param); // Intermittent failures as buffer may not be converted to a string during the lifetime of buffer std::thread t(f,3,buffer); t.detach(); }

I believe requiring most C++ programmers to be conversant with these kinds of silent change of signature is a pretty high bar. This is borne out by my experience teaching C++ to Masters' students, possibly because the reason for this change isn't apparent until one is comfortable writing templates, instead they naturally expect asynchronous functions to obey the same parameter passing rules as ordinary functions.

While this paper is definitely not proposing a change to std::async, we can illustrate how the signature trait could be used to create a "More Perfect Forwarding async" that its arguments with the same signature as its callable does:

// more_perfect_forwarding_async.h #include<iostream> #include<utility> #include<future> #include<type_traits> // Uses "apply" template for unpacking tuple members into arguments written by DRayX at // http://stackoverflow.com/questions/687490/how-do-i-expand-a-tuple-into-variadic-template-functions-arguments #include"apply.h" template<typename Callable, typename Signature> struct Caller; template<typename Callable, typename Result, typename...Args> struct Caller<Callable, Result(Args...)> { Caller(Callable c, Args&&... args) : callable(c), saved_args(std::forward<Args...>(args...)) {} Result operator()() { return apply(callable, saved_args); } std::tuple<Args...> saved_args; Callable callable; }; template<typename Callable, typename... Args> future<typename result_of<Callable(Args...)>::type> more_perfect_forwarding_async(Callable c, Args&&... args) { return std::async(Caller<Callable, typename signature<Callable(Args...)>::type>(c, std::forward<Args>(args)...)); }
Note: This uses complicated template metafunction given by DRayX on StackOverflow to unpack tuple members into a function argument list . See N3416: Packaging Parameter Packs for an alternative approach not requiring metaprogramming.

Now, we can run asynchronous functions without worrying about implicit signature changes

#include"more_perfect_forwarding_async.h" #include<iostream> int func(int &i) { i = 2; return i; } int main() { int i = 5; auto f = more_perfect_forwarding_async(func, i); f.get(); cout << "i = " << i << endl; // Correctly prints 2 }

Pablo Halpern mentions in N3557 that that a theoretical library implementation of fork-join parallelism would benefit from a signature type trait for similar reasons, resulting in fewer programmer errors in asynchronous parameter passing.

Finally, a more perfectly forwarding function binder could also be useful for some applications for more precisely calling the bound function

Wording

Change the end of §20.9.2 [meta.type.synop] to
  template <class> class result_of; // not defined
  template <class F, class... ArgTypes> class result_of<F(ArgTypes...)>;
  template <class> class signature; // not defined
  template <class F, class... ArgTypes> class signature<F(ArgTypes...)>;
} // namespace std
Add the following row to the end of table 57 in @sect;20.9.7.6 [meta.trans.other]
template <class Fn, class... ArgTypes> struct signature<Fn(ArgTypes...)>;Fn shall be a callable type (20.8.1), reference to function, or reference to callable type. The expression INVOKE(declval<Fn>(), declval<ArgTypes>()...) shall be well formed. The member typedef type shall name the type result_of<Fn(ArgTypes...)>::type(T1, T2, ..., Tn) where each Ti is the type of the formal parameter of the callable type (20.8.1) matched by the ith object of declval<ArgTypes>()... in the above INVOKE expression. If Fn is a pointer to member type, T1 will be either a pointer or reference to the containing class (with the same cv-qualification as Fn) according to whether the first element of ArgTypes... is a pointer or reference type.