Why are the returning and non-returning std::functions castable?

Viewed 134

Why does this work?

std::function<int(int)> ret_func = [](int x) { return x; };
std::function<std::string(int)> ret_func1 = [](int x) { return std::to_string(x); };
std::function<void(int)> func = ret_func;
std::function<void(int)> func1 = ret_func1;

Because of type erasure of the functor in the std::function? This cast will not work between any type of function; for example, if you change arguments and try to assign to another std::function why wouldn't this work? Because the return value does not change the type of the functor but arguments do?

Live demo

1 Answers

Don't think of it as casting, think of it as the std::function<int(int)> being assigned to the std::function<void(int)> as a value.

  1. std::function<void(int)> can be assigned any function-like object fun that can be invoked like static_cast<void>(fun(v)) (v being an int).

  2. std::function<int(int)> easily fits that criteria since you can do:

void foo(std::function<int(int)> fun) {
  fun(12);
}

So std::function<int(int)> has to be assignable to std::function<void(int)>. That's all there is to it.

How the library implements this doesn't really matter. It just has to in order to fulfil the API contract.

Disclaimer: This is a tiny bit of an oversimplification. The standard actually has a few provisions for std::function -> std::function assignment, like move-construction and dealing with unassigned std::functionss. However, the semantic principles outlined in this answer still hold as an explanation.

As asked, the relevant parts of the standard:

[func.wrap]

template function(F f); Constraints: F is Lvalue-Callable ([func.wrap.func]) for argument types ArgTypes... and return type R.

A callable type F is Lvalue-Callable for argument types ArgTypes and return type R if the expression INVOKE(declval<F&>(), declval()...), considered as an unevaluated operand, is well-formed ([func.require]).

[func.require]

Define INVOKE(f, t 1 , t 2 , …, t N ) as static_­cast<void>(INVOKE(f, t 1 , t 2 , …, t N )) if R is cv void, otherwise INVOKE(f, t 1 , t 2 , …, t N ) implicitly converted to R.

So since std::function<int(int)> is Lvalue-Callable for void(int), then it is a valid value. Now, in practice, this doesn't actually come into play because of other constructors/assignment operators will intercept it, but the semantics remain.

Related