2591. std::function's member template target() should not lead to undefined behaviour

Section: 23.14.13.2.5 [func.wrap.func.targ] Status: C++17 Submitter: Daniel Krügler Opened: 2016-01-31 Last modified: 2017-09-10

Priority: 3

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

View all issues with C++17 status.

Discussion:

This issue is a spin-off of LWG 2393, it solely focuses on the pre-condition of 23.14.13.2.5 [func.wrap.func.targ] p2:

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

Originally, the author of this issue here had assumed that simply removing the precondition as a side-step of fixing LWG 2393 would be uncontroversial. Discussions on the library reflector indicated that this is not the case, although it seemed that there was agreement on removing the undefined behaviour edge-case.

There exist basically the following positions:

  1. The constraint should be removed completely, the function is considered as having a wide contract.

  2. The pre-condition should be replaced by a Remarks element, that has the effect of making the code ill-formed, if T is a type that is not Lvalue-Callable (20.9.11.2) for parameter types ArgTypes and return type R. Technically this approach is still conforming with a wide contract function, because the definition of this contract form depends on runtime constraints.

Not yet explicitly discussed, but a possible variant of bullet (2) could be:

  1. The pre-condition should be replaced by a Remarks element, that has the effect of SFINAE-constraining this member: "This function shall not participate in overload resolution unless T is a type that is Lvalue-Callable (20.9.11.2) for parameter types ArgTypes and return type R".

The following describes a list of some selected arguments that have been provided for one or the other position using corresponding list items. Unless explicitly denoted, no difference has been accounted for option (3) over option (2).

    1. It reflects existing implementation practice, Visual Studio 2015 SR1, gcc 6 libstdc++, and clang 3.8.0 libc++ do accept the following code:

      #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<void(*)()>() << std::endl;
        boost::function<void(int)> f2(foo);
        std::cout << f2.target<void(*)()>() << std::endl;
      }
      

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

    2. 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 for the corresponding standard component.

    3. There is a consistency argument in regard to the free function template get_deleter

      template<class D, class T> 
      D* get_deleter(const shared_ptr<T>& p) noexcept;
      

      This function also does not impose any pre-conditions on its template argument D.

    1. Programmers have control over the type they're passing to target<T>(). Passing a non-callable type can't possibly retrieve a non-null target, so it seems highly likely to be programmer error. Diagnosing that at compile time seems highly preferable to allowing this to return null, always, at runtime.

    2. If T is a reference type then the return type T* is ill-formed anyway. This implies that one can't blindly call target<T> without knowing what T is.

    3. It has been pointed out that some real world code, boiling down to

      void foo() {}
      
      int main() {
        std::function<void()> f = foo;
        if (f.target<decltype(foo)>()) {
          // fast path
        } else {
          // slow path
        }
      }
      

      had manifested as a performance issue and preparing a patch that made the library static_assert in that case solved this problem (Note that decltype(foo) evaluates to void(), but a proper argument of target() would have been the function pointer type void(*)(), because a function type void() is not any Callable type).

It might be worth adding that if use case (2 c) is indeed an often occurring idiom, it would make sense to consider to provide an explicit conversion to a function pointer (w/o template parameters that could be provided incorrectly), if the std::function object at runtime conditions contains a pointer to a real function, e.g.

R(*)(ArgTypes...) target_func_ptr() const noexcept;

[2016-08 Chicago]

Tues PM: Moved to Tentatively Ready

Proposed resolution:

This wording is relative to N4567.

  1. Change 23.14.13.2.5 [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 Callable (23.14.13.2 [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.