Document number: N3123=10-0113
Date: 2010-08-20
Author: Daniel Krügler
Project: Programming Language C++, Library Working Group
Reply-to: Daniel Krügler

Bringing result_of near to INVOKE

Introduction

Historically, the traits template result_of was proposed as a library component that allows to deduce the return type of function-call like expressions depending on types F and an argument type list expansion T... compressed into a single-type form F(T...), as presented in n1437 and n1454. These papers used the picture of a general function call expression which we can express now - due to variadic templates and decltype - as the type of the following expression:

[A]      std::declval<F>()(std::declval<T>()...)

While the intuitive definition via decltype (which was usually named typeof during those times) concentrated mainly on above expression, from the first proposal on, result_of was suggested to support member function types as well and this manifested in the TR1 specification that requires the deduction of the return type from pointers to member functions. Later, with the acceptance of LWG issue 520, the pointer-to-member support was completed by taking the yet missing pointer to data members into account as well.

In parallel to this development, n1673 and derivatives introduced the concept of callable types and a conceptual macro named INVOKE, which is defined as a more general function call expression as specified above.

A callable type is either a function object type (including pointers to functions) or a pointer to member.

INVOKE is defined in [func.require]/1 as a switch among five call expressions relating a type F and an argument type list exposed as a parameter pack T which might be expandable as T1, Tr... Using the power of decltype combined with std::declval() these expressions can be described as follows:

  1. (std::declval<T1>().*std::declval<F>())(std::declval<Tr>()...)
    when F is a pointer to a member function of a class C and T1 is of object type C or a reference to an object type C or a reference to an object type derived from C;
  2. ((*std::declval<T1>()).*std::declval<F>())(std::declval<Tr>()...)
    when F is a pointer to a member function of a class C and T1 is not one of the types described in the previous item;
  3. std::declval<T1>().*std::declval<F>()
    when F is a pointer to member data of a class C and T1 is an object type C or a reference to an object type C or a reference to an object type derived from C;
  4. (*std::declval<T1>()).*std::declval<F>()
    when F is a pointer to member data of a class C and T1 is not one of the types described in the previous item;
  5. std::declval<F>()(std::declval<T>()...)
    in all other cases.

At the time of the original specification, there did not exist a good way to define INVOKE as a real entity. With the new language facilities of C++0x (including perfect forwarding) it is quite easy to declare a function template that covers all five function-call expression forms shown above as a single uniform signature

template<class F, class... T>
typename result_of<F(T...)>::type invoke(F&& f, T&&... t);

provided result_of<F(T...)>::type is defined in terms of a switch of the five expressions. Given these expressions each of the five different return expressions - where the parameter expansion t... is occasionally expanded as t1, tr... - can be simply written (using the same constraints as described above) as:

  1. (std::forward<T1>(t1).*std::forward<F>(f))(std::forward<T>(tr)...)
  2. ((*std::forward<T1>(t1)).*std::forward<F>(f))(std::forward<T>(tr)...)
  3. std::forward<T1>(t1).*std::forward<F>(f)
  4. (*std::forward<T1>(t1)).*std::forward<F>(f)
  5. std::forward<F>(f)(std::forward<T>(t)...)

The function template invoke as defined above completely covers the functionality of the conceptual macro INVOKE, while result_of<F(T...)>::type describes the return type of each valid INVOKE expression.

Unfortunately, the original support for pointers to members got lost, when several LWG issues (904, 1225) found defects in the wording that mimiced the decltype semantics and in the end replaced the previous specification (that properly honored pointers to member) by the reduced form [A].

Discussion

NB comment US 102 recommends to add the missing support for pointers to member functions and pointers to data members to the type traits result_of. Indeed this addition is needed, because otherwise the specification of several library components that mix INVOKE and result_of is incomplete, among those the specification of:

The introductory text of this paper shows that such a specification can be easily accomplished by the five different call expressions that are already completely described as part of the existing specification of INVOKE in [func.require]/1. The author of this paper could realize INVOKE by implementing a function template invoke as shown above and could define result_of<F(T...)>::type of the required extended form without any problems.

The function template invoke is generally useful and could replace the conceptual INVOKE macro, but this paper does not suggest to add such a new feature to the Standard Library as part of C++0x, because of the lateness in time. Nevertheless the author strongly recommends to add such a functionality in the next possible update of the C++ Standard, because it

Proposed resolution

All wording changes are relative to N3092.

  1. Change Table 53 — Other transformations in [meta.trans.other] as indicated:
    Table 45 — Type relationship predicates
    Template Condition Comments
    template <class Fn,
    class... ArgTypes>
    struct result_of<Fn(ArgTypes...)>;
    Fn shall be a function objectcallable type (20.8[func.def]),
    reference to function, or reference to function objectcallable type.
    The expression
    decltype(declval<Fn>()(declval<ArgTypes>()...))
    decltype(INVOKE(declval<Fn>(),
    declval<ArgTypes>()...))

    shall be well formed.
    The member typedef type shall name the type
    decltype(declval<Fn>()
    (declval<ArgTypes>()...))
    decltype(INVOKE(declval<Fn>(),
    declval<ArgTypes>()...))
  2. Change the example in [meta.trans.other]/4 as indicated (For consistency with other examples the std:: qualifier is suggested to be removed):

    [Example: Given these definitions:

      typedef bool (&PF1)();
      typedef short (*PF2)(long);
    
      struct S {
        operator PF2() const;
        double operator()(char, int&);
        void fn(long) const;
        char data;
      };
      
      typedef void (S::*PMF)(long) const;
      typedef char S::*PMD;
    

    the following assertions will hold:

      static_assert(std::is_same<std::result_of<S(int)>::type, short>::value, "Error!");
      static_assert(std::is_same<std::result_of<S&(unsigned char, int&)>::type, double>::value, "Error!");
      static_assert(std::is_same<std::result_of<PF1()>::type, bool>::value, "Error!");
      static_assert(is_same<result_of<PMF(unique_ptr<S>, int)>::type, void>::value, "Error!");
      static_assert(is_same<result_of<PMD(S)>::type, char&&>::value, "Error!");
      static_assert(is_same<result_of<PMD(const S*)>::type, const char&>::value, "Error!");
    

    end example ]