Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On the various ways of getting the result type of a function

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 ?

like image 806
Lorah Attkins Avatar asked Jan 06 '23 12:01

Lorah Attkins


1 Answers

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(); }
    
like image 159
Barry Avatar answered Jan 17 '23 17:01

Barry