Document number: P0077R1
Supersedes: P0077R0, N4446
Date: 2016-02-12
Project: Programming Language C++, Library Working Group
Reply-to: Agustín Bergé agustinberge@gmail.com

is_callable, the missing INVOKE related trait

0. History

Changes from P0077R0:

  • Add type trait variables is_callable_v, is_nothrow_callable_v.
  • Adjust feature-testing macro for C++17.
  • Add appendix on addressing LWG2393 with is_callable.

Changes from N4446:

  • Add discussion on alternative syntax.
  • Add discussion on additional nothrow trait, add is_nothrow_callable.
  • Add feature-testing macro recomendation.
  • Remove discussion on naming.

1. Introduction

This paper proposes to introduce a new trait to determine whether an INVOKE expression is well formed.

2. Motivation

Starting with C++11, the library introduced the pseudo-macro INVOKE as a way to uniformly handle function objects and member pointers as call expressions. The trait result_of was made to follow INVOKE semantics as well. This left users —who want to follow the precedent set forth by the standard library— with the correct result type but no direct way of obtaining such result, and invoke implementations proliferated.

This was recently rectified by the introduction of invoke to the working draft [N4169]. However, there is still one piece of the puzzle missing, and is the ability to query whether an INVOKE expression is well formed when treated as an unevaluated operand. Such functionality is currently present in the form of C++14 SFINAE-friendly result_of, albeit in a non user-friendly way, and it should be made readily available in trait form for the same reasons invoke was introduced into the library.

The following is an artist depiction of such trait:

template <class T, class R = void, class = void>
struct is_callable
  : false_type
{};
 
template <class T>
struct is_callable<T, void, void_t<result_of_t<T>>>
  : true_type
{};
 
template <class T, class R>
struct is_callable<T, R, void_t<result_of_t<T>>>
  : is_convertible<result_of_t<T>, R>
{};

This trait is implemented in the wild under different names, and the check for a compatible result type is not always present. This post [call-me-maybe] shows how the implementation of such trait has been both improved and simplified by every new standard.

3. Design questions

3.1 Compatible return types

INVOKE comes in two flavors, the primary INVOKE(f, t1, t2, ..., tN) and INVOKE(f, t1, t2, ..., tN, R) defined as INVOKE(f, t1, t2, ..., tN) implicitly converted to R. Both flavors can be supported with a defaulted template argument:

template <class, class R = void> struct is_callable; // not defined
template <class Fn, class... ArgTypes, class R>
  struct is_callable<Fn(ArgTypes...), R>;

However, if only one of those flavors were to be supported there would be no missing functionality, only more work for the user.

3.2 Alternative parameter syntax

Someone suggests alternative parameter syntax, is_callable<Fn, R(Args...)>.

Do we want AB to add consideration of the alternative syntax in the paper?

SF F N A SA
0 6 5 0 0

But consistency between this and invocation_traits (in Fundamentals v1) is important.

The syntax used by the invocation traits in the Library Fundamentals TS is Fn(Args...), which is consistent with the syntax used by std::result_of. The optional trailing R for a checked compatible return type is consistent with the alternative flavor of INVOKE.

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

For consistency with the rest of the standard library, the suggested syntax is Fn(ArgTypes...) to represent an instance of a callable Fn invoked with arguments of type ArgTypes....

3.3 Additional nothrow trait

Add is_noexcept_callable?

SF F N A SA
1 4 4 2 0

Traits that check whether certain expressions involving special member functions are well-formed also ship an additional nothrow trait, that reports the result of applying the noexcept operator to the expression. It is reasonable to provide a similar additional nothrow trait for is_callable, is_nothrow_callable, that reports whether the given INVOKE expression is known not to throw any exceptions.

It should be noted that the standard library does not specify an exception specification for its callable types (like reference_wrapper), but a conforming implementation may add a non-throwing noexcept-specification. The result of is_nothrow_callable when a standard library callable type is involved is thus implementation defined.

4. Feature-testing

For the purposes of SG10, we recommend a feature-testing macro named __cpp_lib_is_callable.

5. Proposed Wording

This wording is relative to [N4567].

Change 20.10.2 [meta.type.synop], header <type_traits> synopsis, as indicated

namespace std {
  [...]
  // 20.10.4.3, type properties:
  [...]
  template <class T> struct has_virtual_destructor;
 
  template <class, class R = void> struct is_callable; // not defined
  template <class Fn, class... ArgTypes, class R>
    struct is_callable<Fn(ArgTypes...), R>;
 
  template <class, class R = void> struct is_nothrow_callable; // not defined
  template <class Fn, class... ArgTypes, class R>
    struct is_nothrow_callable<Fn(ArgTypes...), R>;
 
  [...]
  // 20.10.4.3, type properties
  [...]
  template <class T> constexpr bool has_virtual_destructor_v
    = has_virtual_destructor<T>::value;
 
  template <class T, class R = void> constexpr bool is_callable_v
    = is_callable<T, R>::value;
 
  template <class T, class R = void> constexpr bool is_nothrow_callable_v
    = is_nothrow_callable<T, R>::value;
 
  [...]
}

Change 20.10.4.3 [meta.unary.prop], Table 49 — Type property predicates, add new rows with the following content:

Template:

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

Condition:

The expression INVOKE(declval<Fn>(), declval<ArgTypes>()..., R) is well formed when treated as an unevaluated operand.

Preconditions:

Fn and all types in the parameter pack ArgTypes shall be complete types, (possibly cv-qualified) void, or arrays of unknown bound.

Template:

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

Condition:

is_callable<Fn(ArgTypes...), R>::value is true and the expression INVOKE(declval<Fn>(), declval<ArgTypes>()..., R) is known not to throw any exception.

Preconditions:

Fn and all types in the parameter pack ArgTypes shall be complete types, (possibly cv-qualified) void, or arrays of unknown bound.

6. References

A. An alternative resolution for LWG2393 (informative)

In LWG2132 is_callable is suggested as an alternative to Callable in order to make the wording clear:

STL strongly wants to see an is_callable type trait to clarify the proposed wording.

In LWG2393 the definition of Callable is fixed and renamed to Lvalue-Callable. Here is how the proposed resolution would look like if it were to use is_callable instead, as suggested:

Change 20.9.12.2 [func.wrap.func] p2, as indicated:

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).

Change 20.9.12.2.1 [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);

Remarks: These constructors shall not participate in overload resolution unless f is Callable (20.9.12.2) for argument types ArgTypes... and return type Ris_callable<F&(ArgTypes...), R>::value is true.

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

Remarks: This assignment operator shall not participate in overload resolution unless declval<decay_t<F>&>() is Callable (20.9.12.2) for argument types ArgTypes... and return type R is_callable<decay_t<F>&(ArgTypes...), R>::value is true.

Change 20.9.12.2.5 [func.wrap.func.targ] p2, as indicated:

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

Requires: T shall be a type that is Callable (20.9.12.2) for parameter types ArgTypes and return type R is_callable<remove_cv_t<T>&(ArgTypes...), R>::value shall be true .

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

Note that remove_cv_t is needed here in order to match typeid(T), which strips references and cv-qualifiers but does not decay. Otherwise, it is possible to run into situations in which the target_type() == typeid(T) check returns true but a subsequent target<T>() call yields undefined behavior:

struct foo {
  int operator()() { return 0; }
  void operator()() const {}
};
 
int main() {
  std::function<int()> fun = foo{};
  assert(fun.target_type() == typeid(foo const)); // holds
  fun.target<foo const>(); // undefined behavior
}

There is no need for remove_reference_t given that the return type T* already makes these functions ill-formed for reference types.