std::result_of simple function

user1494506 picture user1494506 · Jul 13, 2012 · Viewed 7.8k times · Source
#include <iostream>
#include <type_traits>

double f(int i)
{
        return i+0.1;
}

struct F
{
        public:
        double operator ()(int i) { return i+0.1; }
};

int
main(int, char**)
{
        std::result_of<F(int)>::type x;     // ok
        // std::result_of<f(int)>::type x; // error: template argument 1 is invalid
        x = 0.1;
        std::cerr << x << std::endl;
}

Please explain why std::result_of<f(int)>::type x; is invalid...

cppreference says "(std::result_of) Deduces the return type of a function call expression at compile type.".

what's the problem?

Answer

Paul Preney picture Paul Preney · Jul 13, 2012

std::result_of<T> requires T to be a type --but not just any type. T must be a function type so this partial specialization of result_of will be used:

template <class Fn, class... ArgTypes> struct result_of<Fn(ArgTypes...)>;

such that:

decltype(INVOKE(declval<Fn>(), declval<ArgTypes>()...))

is well-formed (C++11 20.9.7.6). (INVOKE is defined in 20.8.2.)

The reason std::result_of<f(int)> does not work is because f is not a type --it is an instance of a function type. To declare x to be the return type of f applied to an int, simply write this:

decltype(f(int{})) x;

or if you prefer hard-coding an int:

decltype(f(32)) x;

If the type of f is desired then use:

using FuncPtr = decltype(f);

In the provided code F (i.e., not lower case f) however is a type and so F(int) defines a the type representing the function that returns F accepting an int as an argument. Clearly this is not what F is! F's type is a struct whose instances can use the function call operator. F also has no explicit or implicit constructors taking an int, etc. as well. How can this work? Short answer: Template "magic".

Essentially, the definition of std::result_of takes the type, F(int) and separates the return type from the argument types so it can determine which case of INVOKE() would allow it to work. The cases of INVOKE are:

  1. F is a pointer to a member function for some class T
  2. If there is only one argument, F is a pointer to a data member of class T, or,
  3. An instance of F can be used as a function, i.e.,
declval<F>()(declval<int>())

which can be a normal function call or some type of functor (e.g., like your example).

Once this has been determined result_of can then determine the return type of the valid expression. This is what is returned via result_of's type member.

The beautiful thing about this is that the user of result_of need not know anything about how this actually works. The only thing one has to understand is that result_of needs a function TYPE. If one is using names that are not types within code (e.g., f) then decltype will need to be used to obtain the type of an expression with such.

Finally, part of the reason why f cannot be considered as a type is because template parameters also allow constant values and f is a constant function pointer value. This is easily demonstrated (using the question's definition of f):

template <double Op(int)>
double invoke_op(int i)
{
  return Op(i);
}

and later:

std::cout << invoke_op<f>(10) << std::endl;

So to obtain the return value type of an expression properly invoking f with some int one would write:

decltype(f(int{}))

(Note: f is never called: the compiler simply uses the expression within decltype to determine its result i.e., its return value in this instance.)