2393. std::function's Callable definition is broken

Section: 22.10.17.3 [func.wrap.func] Status: C++17 Submitter: Daniel Krügler Opened: 2014-06-03 Last modified: 2017-07-30

Priority: 2

View all other issues in [func.wrap.func].

View all issues with C++17 status.

Discussion:

The existing definition of std::function's Callable requirements provided in 22.10.17.3 [func.wrap.func] p2,

A callable object f of type F is Callable for argument types ArgTypes and return type R if the expression INVOKE(f, declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.9.2).

is defective in several aspects:

  1. The wording can be read to be defined in terms of callable objects, not of callable types.

  2. Contrary to that, 22.10.17.3.6 [func.wrap.func.targ] p2 speaks of "T shall be a type that is Callable (20.9.11.2) for parameter types ArgTypes and return type R."

  3. The required value category of the callable object during the call expression (lvalue or rvalue) strongly depends on an interpretation of the expression f and therefore needs to be specified unambiguously.

The intention of original proposal (see IIIa. Relaxation of target requirements) was to refer to both types and values ("we say that the function object f (and its type F) is Callable […]"), but that mental model is not really deducible from the existing wording. An improved type-dependence wording would also make the sfinae-conditions specified in 22.10.17.3.2 [func.wrap.func.con] p8 and p21 ("[…] shall not participate in overload resolution unless f is Callable (20.9.11.2) for argument types ArgTypes... and return type R.") easier to interpret.

My understanding always had been (see e.g. Howard's code example in the 2009-05-01 comment in LWG 815), that std::function invokes the call operator of its target via an lvalue. The required value-category is relevant, because it allows to reflect upon whether an callable object such as

struct RVF 
{
  void operator()() const && {}
};

would be a feasible target object for std::function<void()> or not.

Clarifying the current Callable definition seems also wise to make a future transition to language-based concepts easier. A local fix of the current wording is simple to achieve, e.g. by rewriting it as follows:

A callable object f of type (22.10.3 [func.def]) F is Callable for argument types ArgTypes and return type R if the expression INVOKE(fdeclval<F&>(), declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.9.2).

It seems appealing to move such a general Callable definition to a more "fundamental" place (e.g. as another paragraph of 22.10.3 [func.def]), but the question arises, whether such a more general concept should impose the requirement that the call expression is invoked on an lvalue of the callable object — such a special condition would also conflict with the more general definition of the result_of trait, which is defined for either lvalues or rvalues of the callable type Fn. In this context I would like to point out that "Lvalue-Callable" is not the one and only Callable requirement in the library. Counter examples are std::thread, call_once, or async, which depend on "Rvalue-Callable", because they all act on functor rvalues, see e.g. 32.4.3.3 [thread.thread.constr]:

[…] The new thread of execution executes INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...) […]

For every callable object F, the result of DECAY_COPY is an rvalue. These implied rvalue function calls are no artifacts, but had been deliberately voted for by a Committee decision (see LWG 2021, 2011-06-13 comment) and existing implementations respect these constraints correctly. Just to give an example,

#include <thread>

struct LVF 
{
  void operator()() & {}
};

int main()
{
  LVF lf;
  std::thread t(lf);
  t.join();
}

is supposed to be rejected.

The below presented wording changes are suggested to be minimal (still local to std::function), but the used approach would simplify a future (second) conceptualization or any further generalization of Callable requirements of the Library.

[2015-02 Cologne]

Related to N4348. Don't touch with a barge pole.

[2015-09 Telecon]

N4348 not going anywhere, can now touch with or without barge poles
Ville: where is Lvalue-Callable defined?
Jonathan: this is the definition. It's replacing Callable with a new term and defining that. Understand why it's needed, hate the change.
Geoff: punt to an LWG discussion in Kona

[2015-10 Kona]

STL: I like this in general. But we also have an opportunity here to add a precondition. By adding static assertions, we can make implementations better. Accept the PR but reinstate the requirement.

MC: Status Review, to be moved to TR at the next telecon.

[2015-10-28 Daniel comments and provides alternative wording]

The wording has been changed as requested by the Kona result. But I would like to provide the following counter-argument for this changed resolution: Currently the following program is accepted by three popular Standard libraries, Visual Studio 2015, gcc 6 libstdc++, and clang 3.8.0 libc++:

#include <functional>
#include <iostream>
#include <typeinfo>
#include "boost/function.hpp"

void foo(int) {}

int main() {
  std::function<void(int)> f(foo);
  std::cout << f.target<double>() << std::endl;
  boost::function<void(int)> f2(foo);
  std::cout << f2.target<double>() << std::endl;
}

and outputs the implementation-specific result for two null pointer values.

Albeit this code is not conforming, it is probable that similar code exists in the wild. The current boost documentation does not indicate any precondition for calling the target function, so it is natural that programmers would expect similar specification and behaviour.

Standardizing the suggested change requires a change of all implementations and I don't see any advantage for the user. With that change previously working code could now cause instantiation errors, I don't see how this could be considered as an improvement of the status quo. The result value of target is always a pointer, so a null-check by the user code is already required, therefore I really see no reason what kind of problem could result out of the current implementation behaviour, since the implementation never is required to perform a C cast to some funny type.

Previous resolution [SUPERSEDED]:

This wording is relative to N3936.

  1. Change 22.10.17.3 [func.wrap.func] p2 as indicated:

    -2- A callable object f of type (22.10.3 [func.def]) F is Lvalue-Callable for argument types ArgTypes and return type R if the expression INVOKE(fdeclval<F&>(), declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.9.2).

  2. Change 22.10.17.3.2 [func.wrap.func.con] p8+p21 as indicated:

    template<class F> function(F f);
    template <class F, class A> function(allocator_arg_t, const A& a, F f);
    

    […]

    -8- Remarks: These constructors shall not participate in overload resolution unless fF is Lvalue-Callable (20.9.11.2) for argument types ArgTypes... and return type R.

    […]

    template<class F> function& operator=(F&& f);
    

    […]

    -21- Remarks: This assignment operator shall not participate in overload resolution unless declval<typename decay<F>::type&>()decay_t<F> is Lvalue-Callable (20.9.11.2) for argument types ArgTypes... and return type R.

  3. Change 22.10.17.3.6 [func.wrap.func.targ] p2 as indicated: [Editorial comment: Instead of adapting the preconditions for the naming change I recommend to strike it completely, because the target() functions do not depend on it; the corresponding wording exists since its initial proposal and it seems without any advantage to me. Assume that some template argument T is provided, which does not satisfy the requirements: The effect will be that the result is a null pointer value, but that case can happen in other (valid) situations as well. — end comment]

    template<class T> T* target() noexcept;
    template<class T> const T* target() const noexcept;
    

    -2- Requires: T shall be a type that is Callable (20.9.11.2) for parameter types ArgTypes and return type R.

    -3- Returns: If target_type() == typeid(T) a pointer to the stored function target; otherwise a null pointer.

[2015-10, Kona Saturday afternoon]

GR explains the current short-comings. There's no concept in the standard that expresses rvalue member function qualification, and so, e.g. std::function cannot be forbidden from wrapping such functions. TK: Although it wouldn't currently compile.

GR: Implementations won't change as part of this. We're just clearing up the wording.

STL: I like this in general. But we also have an opportunity here to add a precondition. By adding static assertions, we can make implementations better. Accept the PR but reinstate the requirement.

JW: I hate the word "Lvalue-Callable". I don't have a better suggestion, but it'd be terrible to teach. AM: I like the term. I don't like that we need it, but I like it. AM wants the naming not to get in the way with future naming. MC: We'll review it.

TK: Why don't we also add Rvalue-Callable? STL: Because nobody consumes it.

Discussion whether "tentatively ready" or "review". The latter would require one more meeting. EF: We already have implementation convergence. MC: I worry about a two-meeting delay. WEB: All that being said, I'd be slightly more confident with a review since we'll have new wording, but I wouldn't object. MC: We can look at it in a telecon and move it.

STL reads out email to Daniel.

Status Review, to be moved to TR at the next telecon.

[2016-01-31, Daniel comments and suggests less controversive resolution]

It seems that specifically the wording changes for 22.10.17.3.6 [func.wrap.func.targ] p2 prevent this issue from making make progress. Therefore the separate issue LWG 2591 has been created, that focuses solely on this aspect. Furtheron the current P/R of this issue has been adjusted to the minimal possible one, where the term "Callable" has been replaced by the new term "Lvalue-Callable".

Previous resolution II [SUPERSEDED]:

This wording is relative to N4527.

  1. Change 22.10.17.3 [func.wrap.func] p2 as indicated:

    -2- A callable object f of type (22.10.3 [func.def]) F is Lvalue-Callable for argument types ArgTypes and return type R if the expression INVOKE(fdeclval<F&>(), declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.9.2).

  2. Change 22.10.17.3.2 [func.wrap.func.con] p8+p21 as indicated:

    template<class F> function(F f);
    template <class F, class A> function(allocator_arg_t, const A& a, F f);
    

    […]

    -8- Remarks: These constructors shall not participate in overload resolution unless fF is Lvalue-Callable (20.9.11.2) for argument types ArgTypes... and return type R.

    […]

    template<class F> function& operator=(F&& f);
    

    […]

    -21- Remarks: This assignment operator shall not participate in overload resolution unless declval<typename decay<F>::type&>()decay_t<F> is Lvalue-Callable (20.9.11.2) for argument types ArgTypes... and return type R.

  3. Change 22.10.17.3.6 [func.wrap.func.targ] p2 as indicated:

    template<class T> T* target() noexcept;
    template<class T> const T* target() const noexcept;
    

    -2- Remarks: If T is a type that is not Lvalue-Callable (20.9.11.2) for parameter types ArgTypes and return type R, the program is ill-formedRequires: T shall be a type that is Callable (20.9.11.2) for parameter types ArgTypes and return type R.

    -3- Returns: If target_type() == typeid(T) a pointer to the stored function target; otherwise a null pointer.

[2016-03 Jacksonville]

Move to Ready.

Proposed resolution:

This wording is relative to N4567.

  1. Change 22.10.17.3 [func.wrap.func] p2 as indicated:

    -2- A callable object f of type (22.10.3 [func.def]) F is Lvalue-Callable for argument types ArgTypes and return type R if the expression INVOKE(fdeclval<F&>(), declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.9.2).

  2. Change 22.10.17.3.2 [func.wrap.func.con] p8+p21 as indicated:

    template<class F> function(F f);
    template <class F, class A> function(allocator_arg_t, const A& a, F f);
    

    […]

    -8- Remarks: These constructors shall not participate in overload resolution unless fF is Lvalue-Callable (22.10.17.3 [func.wrap.func]) for argument types ArgTypes... and return type R.

    […]

    template<class F> function& operator=(F&& f);
    

    […]

    -21- Remarks: This assignment operator shall not participate in overload resolution unless declval<typename decay<F>::type&>()decay_t<F> is Lvalue-Callable (22.10.17.3 [func.wrap.func]) for argument types ArgTypes... and return type R.

  3. Change 22.10.17.3.6 [func.wrap.func.targ] p2 as indicated:

    template<class T> T* target() noexcept;
    template<class T> const T* target() const noexcept;
    

    -2- Requires: T shall be a type that is Lvalue-Callable (22.10.17.3 [func.wrap.func]) for parameter types ArgTypes and return type R.

    -3- Returns: If target_type() == typeid(T) a pointer to the stored function target; otherwise a null pointer.