N3866
Revision of N3729
2014-01-19
Mike Spertus
mike_spertus@symantec.com

Invocation type traits (Rev. 2)

Overview

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

For example, if we define C as struct C { int operator()(double d, int i); int operator()(double d1, double d2); }; then std::invocation_type<C(int, int)>::type would be int(double, int). More precisely the return type of invocation_type<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.

The main difference between this and previous revisions is that we add two variants of the invocation_type.

Example application: More perfect forwarding

This section gives several examples of using std::invocation_type 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 invocation_type 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 invocation_type<Callable(Args...)>::type> (c, std::forward<Args>(args)...)); }
Note: This uses a complicated template metafunction given by DRayX on StackOverflow to unpack tuple members into a function argument list . See N3728: Packaging Parameter Packs (Rev. 2) for an alternative approach not requiring advanced 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

Rvalue references

In the more_perfect_forwarding_async.h example above, we appear risk storing references to temporaries: #include "more_perfect_forwarding_async.h" #include <memory> struct S { void m(); }; S s(); void f() { more_perfect_async( &S::m, s()); // s() will no longer exist when async executes. §12.2p5 }

In order to avoid such dangling references, if the callable parameter to an invocation_type is a member pointer and the first parameter is an rvalue, we decay the the first parameter of the invocation type. The raw_invocation_type type trait is provided if we really want a reference to the temporary (e.g., if one knows the lifetime of the temporary is long enough for our use case).

Thanks to Peter Dimov for helping me clarify my understanding of this issue.

Selecting the operator()

It should be noted that while the above facility gives the signature of the function call operator called, it actually figures out which member is called, and then discards all information other than the signature. Because of the complicated transformations needed to calculate the invocation_type, it may be impossible to actually find the function call operator that was matched (E.g., if defaulted parameters were used). It seems perverse to throw away potentially useful "Overloading Reflection", as alluded to in n3492. We should actually be able to say what was called, not just it's signature. This could be made available through a function_call_operator type trait whose value member points to the function call operator that was selected by overload resolution. struct A { void operator()(); int operator()(int i); }; auto fco = function_call_operator::value; A a; a.*fco(7);

Wording

Modify §20.9.2p1 [func.require] as follows:
Define INVOKE(f, t1, t2, ..., tN)and the corresponding invocation parameters as in the following, in which U1 denotes T1 & if t1 is an lvalue or T1 && if t1 is an rvalue where T1 is the possibly cv-qualified type of t1:

— (t1.*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;. The invocation parameters are U1 followed by the parameters of f matched by t2, ..., tN.

— ((*t1).*f)(t2, ..., tN) when f is a pointer to a member function of a class T and tT1 is not one of the types described in the previous item;.The invocation parameters are U1 followed by the parameters of f matched by t2, ..., tN.

— t1.*f when N == 1 and f is a pointer to member data of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;. The invocation parameter is U1.

— (*t1).*f when N == 1 and f is a pointer to member data of a class T and tT1 is not one of the types described in the previous item;. The invocation parameter is U1.

— f(t1, t2, ..., tN) if f is a class object. The invocation parameters are the parameters matching t1, ..., tN of the best viable function (13.3.3) for the arguments t1, ..., tN among the function call operators of f.

— f(t1, t2, ..., tN) in all other cases. The invocation parameters are the parameters of f matching t1, ... tN.

In all of the above cases, if an argument tI matches the ellipsis in the function's parameter-declaration-clause, the corresponding invocation parameter is defined to be the result of applying the default argument promotions (5.2.2) to tI.

[example
Assume S is defined as

struct S { int f(double const &) const; void operator()(int, int); void operator()(char const *, int i = 2, int j = 3); void operator()(...); };
The invocation parameters of INVOKE(&S::f, S(), 3.5) are (S &&, double const &).
The invocation parameters of INVOKE(S(), 1, 2) are (int, int).
The invocation parameters of INVOKE(S(), "abc", 5) are (const char *, int). // Defaulted parameter j does not correspond to an argument
The invocation parameters of INVOKE(S(), locale(), 5) are (locale, int). // Arguments corresponding to ellipsis maintain their types
—end example ]

Change the end of §20.10.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 invocation_type; // not defined
  template <class F, class... ArgTypes> class invocation_type<F(ArgTypes...)>;
  template <class> class raw_invocation_type; // not defined
  template <class F, class... ArgTypes> class raw_invocation_type<F(ArgTypes...)>;
  template <class> class function_call_operator; // not defined
  template <class F, class... ArgTypes> class function_call_operator<F(ArgTypes...)>;
} // namespace std
Add the following rows to the end of table 57 in §20.10.7.6 [meta.trans.other]
template <class Fn, class... ArgTypes> struct raw_invocation_type<Fn(ArgTypes...)>;Fn and all types in the parameter pack ArgTypes shall be complete types, (possibly cv-qualified) void, or arrays of unknown bound. If the expression INVOKE(declval<Fn>(), declval<ArgTypes>()...) is well formed when treated as an unevaluated operand (Clause 5), The member typedef type shall name the function type R(T1, T2, ...) where R denotes result_of<Fn(ArgTypes...)>::type and the types Ti are the invocation parameters (20.8.2) of INVOKE(declval<Fn>(), declval<ArgTypes>()...); otherwise, there shall be no member type. Access checking is performed as if in a context unrelated to Fn and ArgTypes. Only the validity of the immediate context of the expression is considered. [ Note: The compilation of the expression can result in side effects such as the instantiation of class template specializations and function template specializations, the generation of implicitly-defined functions, and so on. Such side effects are not in the "immediate context" and can result in the program being ill-formed. —end note]
template <class Fn, class... ArgTypes> struct invocation_type<Fn(ArgTypes...)>;Fn and all types in the parameter pack ArgTypes shall be complete types, (possibly cv-qualified) void, or arrays of unknown bound. If raw_invocation_type<Fn(ArgTypes...)>::type is the function type R(T1, T2, ...) and Fn is a pointer to member type and T1 is an rvalue reference, then R(decay<T1>::type, T2, ...). Otherwise, raw_invocation_type<Fn(ArgTypes...)>::type.
template <class Fn, class... ArgTypes> struct function_call_operator<Fn(ArgTypes...)>;Fn and all types in the parameter pack ArgTypes shall be complete types, (possibly cv-qualified) void, or arrays of unknown bound. If Fn is of class type and there exists a best-viable function (13.3.3) fco for the arguments declval<ArgTypes>()... among the function call operators of Fn, then value will be fco. Otherwise, there will be no value member.