4

In a context where the type of the result of a function call must be deduced, C++ seems to be more that happy to help us, providing (at least to my knowledge the following) two solutions :

  • The result of type trait :

    std::result_of<F(Args...)>::type
    
  • A core language syntax :

    decltype(std::declval<F>()(std::declval<Args>()...); 
    

My question is, are the any differences between the two? Is there a context where one cannot be substituted by the other and if not why did we need a type trait to do something the language could do out of the box ?

Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • Actually, I'm not sure it's a dupe. [That one](http://stackoverflow.com/q/2689709/2069064) is generically about `decltype`. This one is specifically in the context of a function call. – Barry Mar 07 '16 at 00:11

3 Answers3

5

There are three differences.

  1. Initially, std::result_of was not required to be SFINAE-friendly. So if were to use it in a context to verify that F was callable with Args..., it would give you a hard error whereas decltype with std::declval would simply cause the likely intended substitution failure. N3462 fixed the specification to make it SFINAE-friendly.

    On a compliant C++14 compiler though, both are SFINAE friendly. std::result_of_t<Fn(ArgsTypes...)> is actually defined in terms of the latter. From [meta.trans.other]:

    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 type decltype(INVOKE (declval<Fn>(), declval<ArgTypes>()...)); otherwise, there shall be no member type.

  2. As made clear in the language of the definition of result_of, the type Fn can be anything INVOKE-able, whereas explicitly calling std::declval<F>() only works on functions and function objects. So if you had:

    using F = decltype(&X::bar);
    using T1 = std::result_of_t<F(X*)>;                        // ok
    using T2 = decltype(std::declval<F>()(std::declval<X*>()); // not ok
    
  3. For some types, std::result_of doesn't actually work due to it being illegal to specify certain type-ids (see my related question). Those types are using functions for F (as opposed to pointers/references to function) or using abstract classes for any of the Args.... So consider something seemingly innocuous like:

    template <class F, class R = std::result_of_t<F()>>
    R call(F& f) { return f(); }
    
    int foo();
    call(foo); // error, unresolved overload etc.
    

    This fails SFINAE (at least it's not a hard error) because we'd have to form the type int()() - a nullary function returning a function. Now, technically, we wrote our code incorrectly. It should've been:

    template <class F, class R = std::result_of_t<F&()>>
    R call_fixed(F& f) { return f(); }
    

    This works. But had we made the same mistake with declval, we'd've been fine:

    template <class F, class R = decltype(std::declval<F>()())>
    R call_declval(F& f) { return f(); }
    
Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977
  • A third one: If `F` or one of `Args` is abstract, or if `F` is a function type, `result_of` is invalid, but `decltype`/`declval` works correctly. (The fix is to use `result_of`. – T.C. Mar 07 '16 at 06:24
  • @Barry I believe forming the type "function returning function" or "function [taking abstract type by value] returning abstract type by value" is not possible. – Columbo Mar 07 '16 at 13:16
  • @Columbo That's pretty unfortunate. – Barry Mar 07 '16 at 13:56
  • @Barry Perhaps `result_of` was introduced before variadic templates were (it was initially proposed in [2003](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1454.html)). To pass `result_of` the information it required, they figured using a composite function type is both efficient (as opposed to some tuple-esque class template) and has neat syntax. The latter still holds and IMO outweighs those corner cases. – Columbo Mar 07 '16 at 14:10
  • @Columbo Yeah I don't entirely understand that though. [Care to elaborate?](http://stackoverflow.com/q/35848012/2069064) – Barry Mar 07 '16 at 15:50
  • What @Columbo said. These are not valid types and attempting to form one actually would cause a deduction failure in SFINAE context, which can lead to some very entertaining results if you are using `result_of` for SFINAE. – T.C. Mar 07 '16 at 16:24
  • @T.C. Is that just an issue with how `result_of` is specified then? So yes, it's SFINAE friendly, except in some cases where it's actually not? And it's specified as `decltype(INVOKE(...))` being valid --> `result_of` has that type, but that's not true for these edge cases due to the "cutesy" way `result_of` is specified. – Barry Mar 07 '16 at 16:27
  • It's SFINAE friendly...if you pass it a valid type. `result_of` is just like `is_reference`. – T.C. Mar 07 '16 at 16:28
3

result_of yields the return type of std::invoke when applied to the given types - it's even used in invoke's declared return type. However, invoke covers far more scenarios than just ordinary non-member function calls. The expression in decltype(…) you showed is just one of many that result_of examines.

Thus your options are only equivalent if F is guaranteed to be a function object type. Otherwise result_of<…> may be valid in cases in which decltype(…) is not:

struct A {int i;};
decltype(std::declval<int A::*>()(std::declval<A>())) // error
std::result_of_t<int A::*(A)> // = int&&, treated as a member access

Demo. (Note that result_of is SFINAE friendly as of C++14, i.e. invalid types cause a smooth deduction failure as for decltype.)

Columbo
  • 60,038
  • 8
  • 155
  • 203
1

Take some C++ programmers who aren't very experienced and ask them to guess what each of the code snippets does.

How many get the first right? How many get the second right?

Giving this operation a name makes it much more readable.

Now take some slightly more experienced C++ programmers and ask them to get the result type of a function call with and without using result_of. How many get them right and how long does it take them?

This is already non-trivial metaprogramming, and having it encapsulated makes it easier to discover with Google and easier to get right.

Also, the first version is quite a bit shorter.

So result_of is very useful, and since the standard library itself needs it quite a few times, making it available to the programmer too is trivial.

Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157