Document number: N4446
Date: 2015-04-09
Project: Programming Language C++, Library Evolution Working Group
Reply-to: Agustín Bergé agustinberge@gmail.com

The missing INVOKE related trait

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 precedence 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 Naming

The property this trait determines is usually referred to as whether something is callable with given arguments [citation needed], a name dating from before INVOKE was introduced. But perhaps callable is not the most appropriate name, as the standard already defines a number of things by that name:

20.9.1 [func.def]/3 A callable type is a function object type or a pointer to member.

20.9.1 [func.def]/4 A callable object is an object of a callable type.

These definitions of callable do not represent what the trait would do. Knowing whether some object is a function object or pointer to member is meaningless, as it does not tell whether operating on such an object would be well formed.

The following definition of —proper cased— Callable, introduced and used only by std::function, matches exactly what the trait would do:

20.9.12.2 [func.wrap.func]/2 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, is well formed.

The rest of the standard library simply requires INVOKE expressions being well formed.

The ranges proposal [N4128] uses the term invokable instead, for things that work with INVOKE, and defines an Invokable concept.

3.2 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>;

[Note: This assumes that the resolution for LWG2420 makes INVOKE(f, t1, t2, ..., tN, void) discard the return type. -end note]

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

4. Proposed Wording

This wording is relative to [N4296].

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 is_nothrow_destructible;
  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>;
 
  [...]
}

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

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.

5. References